diff --git a/.gitignore b/.gitignore index ff14b51a369..6674d767513 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ installer/kvm-config.sh docs/book/src/_build /.vs +.venv diff --git a/conf/default/web.conf.default b/conf/default/web.conf.default index f829ca02eb7..5cb2648e239 100644 --- a/conf/default/web.conf.default +++ b/conf/default/web.conf.default @@ -7,8 +7,8 @@ enabled = no captcha = no 2fa = no # To enable Oauth check https://django-allauth.readthedocs.io and web/web/settings.py. -# Allow only SSO for users with specific domain. Can be allow to all if empty. -social_auth_email_domain = example.com +# Allow only SSO for users with a specific email domain. Leave blank to allow any. +social_auth_email_domain = [registration] enabled = no @@ -30,6 +30,13 @@ disposable_domain_list = data/safelist/disposable_domain_list.txt [general] timezone = UTC +# Set to yes only when CAPE is served behind a reverse proxy (e.g. nginx) that +# terminates TLS and strips/overwrites X-Forwarded-Proto / X-Forwarded-Host from +# clients. Enables Django's SECURE_PROXY_SSL_HEADER + USE_X_FORWARDED_HOST so +# request.is_secure() and absolute URLs (incl. the OIDC redirect_uri) are correct. +# Leave no for a directly-exposed app — trusting forwarded headers would allow +# proto/host spoofing. +behind_proxy = no # Prescan new file tasks with YARA for sample identification and custom execution # Useful to set options, tags, timeout, etc for packers/obfuscators/cryptors yara_recon = no @@ -150,6 +157,47 @@ github = no gitlab = no twitter = no + +# OpenID Connect SSO. Generic — works with Okta / Azure AD / Auth0 / Google +# Workspace / Keycloak or any OIDC-compliant IdP via django-allauth's +# openid_connect provider. Set enabled = yes and fill in the fields below. +[oauth_oidc] +enabled = no +# Arbitrary identifier; becomes part of the callback URL: +# /accounts/oidc//login/callback/ +# Must match the redirect URI registered in your IdP app. +provider_id = oidc +# Display name shown on the login button. +name = SSO +# OAuth2 client credentials from your IdP app. +client_id = +client_secret = +# IdP discovery root — the /.well-known/openid-configuration is appended +# automatically. Examples: +# Okta: https:///oauth2/default +# Azure: https://login.microsoftonline.com//v2.0 +# Auth0: https:// +# Keycloak: https:///realms/ +server_url = +# Group-claim -> Django role mapping. The IdP must include group names in the +# ID token under `groups_claim` (default: "groups"). +# required_groups: if set, only users in at least one of these groups get a +# CAPE account provisioned. Leave blank to allow any IdP-authenticated user. +# admin_groups / superadmin_groups: membership maps to is_staff / is_superuser +# and is reconciled on every login (removal from the group demotes the user). +# Leave both blank to manage roles manually in Django. +required_groups = +admin_groups = +superadmin_groups = +groups_claim = groups +# Optional: periodic Okta account-status reconciliation (okta_user_sync +# management command, run via systemd timer). When a user with active API keys +# is no longer ACTIVE in Okta, they are deactivated locally and their keys are +# revoked. Requires an Okta admin API base URL and an SSWS token with the +# okta.users.read scope. Leave blank to disable. +admin_api_url = +admin_api_token = + [display_browser_martians] enabled = no diff --git a/installer/cape2.sh b/installer/cape2.sh index fc363d3827b..11d3f2cea30 100755 --- a/installer/cape2.sh +++ b/installer/cape2.sh @@ -64,15 +64,28 @@ TOR_SOCKET_TIMEOUT="60" CAPE_ROOT="${CAPE_ROOT:-/opt/CAPEv2}" USE_UV=${USE_UV:-false} -PYTHON_MGR="/etc/poetry/bin/poetry" -PYTHON_MGR_CMD="run" -PYTHON_MGR_INSTALL="install" + +set_python_mgr() { + if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then + PYTHON_MGR="/usr/local/bin/uv" + PYTHON_MGR_CMD="run" + PYTHON_MGR_PIP="pip" + PYTHON_MGR_INSTALL_PYPROJECT="sync --no-install-project" + else + PYTHON_MGR="/etc/poetry/bin/poetry" + PYTHON_MGR_CMD="run" + PYTHON_MGR_PIP="run pip" + PYTHON_MGR_INSTALL_PYPROJECT="install" + fi +} # if a config file is present, read it in if [ -f "./cape-config.sh" ]; then . ./cape-config.sh fi +set_python_mgr + UBUNTU_VERSION=$(lsb_release -rs) OS="$(uname -s)" MAINTAINER="$(whoami) "_"$(hostname)" @@ -666,11 +679,7 @@ function redsocks2() { function distributed() { echo "[+] Configure distributed configuration" sudo apt-get install -y uwsgi uwsgi-plugin-python3 nginx 2>/dev/null - if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then - sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_CMD pip install flask flask-restful flask-sqlalchemy requests" - else - sudo -u ${USER} bash -c "$PYTHON_MGR $PYTHON_MGR_CMD pip install flask flask-restful flask-sqlalchemy requests" - fi + sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_PIP install flask flask-restful flask-sqlalchemy requests" sudo cp $CAPE_ROOT/uwsgi/capedist.ini /etc/uwsgi/apps-available/cape_dist.ini sudo ln -s /etc/uwsgi/apps-available/cape_dist.ini /etc/uwsgi/apps-enabled @@ -763,6 +772,8 @@ file-store.enabled: yes EOF sed -i '$a include:\n - cape.yaml\n' /etc/suricata/suricata.yaml + getent group pcap || groupadd --system pcap + getent group suricata || groupadd --system suricata usermod -aG pcap suricata usermod -aG suricata "${USER}" # sudo chmod -R g+w /var/log/suricata/ @@ -788,7 +799,7 @@ function install_yara_x() { sudo -u ${USER} git clone https://github.com/VirusTotal/yara-x cd yara-x || return sudo -u ${USER} bash -c 'source "$HOME/.cargo/env" ; cargo install --path cli' - sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_CMD pip install yara-x + sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_PIP install yara-x } function install_yara_python() { @@ -804,21 +815,12 @@ function install_yara_python() { # This replaces the legacy setup.py build approach # Install from PyPI - if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then - sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR pip install yara-python \ - --no-binary :all: \ - --config-settings=\"--global-option=build\" \ - --config-settings=\"--global-option=--enable-cuckoo\" \ - --config-settings=\"--global-option=--enable-magic\" \ - --config-settings=\"--global-option=--enable-profiling\"" - else - sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT $PYTHON_MGR_CMD pip install yara-python \ - --no-binary :all: \ - --config-settings="--global-option=build" \ - --config-settings="--global-option=--enable-cuckoo" \ - --config-settings="--global-option=--enable-magic" \ - --config-settings="--global-option=--enable-profiling" - fi + sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT $PYTHON_MGR_PIP install yara-python \ + --no-binary :all: \ + --config-settings="--global-option=build" \ + --config-settings="--global-option=--enable-cuckoo" \ + --config-settings="--global-option=--enable-magic" \ + --config-settings="--global-option=--enable-profiling" # Install from local source (commented out) # sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT $PYTHON_MGR_CMD pip install /tmp/yara-python \ @@ -909,16 +911,7 @@ function install_libvirt() { export_path="${temp_export_path%/*}/" export PKG_CONFIG_PATH=$export_path - # Run build and install within the project environment - # We use sudo -u cape ... to install into the user's environment managed by poetry/uv/pip - if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then - # sudo -u ${USER} bash -c "export PKG_CONFIG_PATH=$export_path; cd $CAPE_ROOT && $PYTHON_MGR pip install /tmp/libvirt-python-${LIB_VERSION}" - sudo -u ${USER} bash -c "export PKG_CONFIG_PATH=$export_path; cd $CAPE_ROOT && $PYTHON_MGR pip install libvirt-python==${LIB_VERSION}" - elif [ "$PYTHON_MGR" = "/etc/poetry/bin/poetry" ]; then - sudo -u ${USER} bash -c "export PKG_CONFIG_PATH=$export_path; $PYTHON_MGR --directory $CAPE_ROOT $PYTHON_MGR_CMD pip install libvirt-python==${LIB_VERSION}" - else - sudo -u ${USER} bash -c "export PKG_CONFIG_PATH=$export_path; pip3 install libvirt-python==${LIB_VERSION}" - fi + sudo -u ${USER} bash -c "export PKG_CONFIG_PATH=$export_path; $PYTHON_MGR --directory $CAPE_ROOT $PYTHON_MGR_PIP install libvirt-python==${LIB_VERSION}" } function install_mongo(){ @@ -1061,11 +1054,7 @@ function install_capa() { cd capa || return git pull git submodule update --init rules - if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then - sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_CMD pip install /tmp/capa" - else - sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_CMD pip install /tmp/capa - fi + sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_PIP install /tmp/capa cd $CAPE_ROOT if [ -d /tmp/capa ]; then sudo rm -rf /tmp/capa @@ -1367,6 +1356,9 @@ function install_CAPE() { git clone https://github.com/kevoreilly/CAPEv2/ "$CAPE_ROOT" fi chown ${USER}:${USER} -R "$CAPE_ROOT"/ + if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then + sudo -u ${USER} /usr/local/bin/uv venv "$CAPE_ROOT/.venv" + fi #chown -R root:${USER} /usr/var/malheur/ #chmod -R =rwX,g=rwX,o=X /usr/var/malheur/ # Adapting owner permissions to the ${USER} path folder @@ -1380,7 +1372,7 @@ function install_CAPE() { echo "[-] pyproject.toml not found in $CAPE_ROOT" return fi - sudo -u ${USER} bash -c "export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring; CRYPTOGRAPHY_DONT_BUILD_RUST=1 $PYTHON_MGR pip install -r pyproject.toml" + sudo -u ${USER} bash -c "cd $CAPE_ROOT && export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring; export CRYPTOGRAPHY_DONT_BUILD_RUST=1; $PYTHON_MGR $PYTHON_MGR_INSTALL_PYPROJECT" if [ "$DISABLE_LIBVIRT" -eq 0 ]; then # Integrated libvirt install @@ -1471,11 +1463,12 @@ function install_systemd() { fi if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then + # Remove poetry config ExecStartPre lines BEFORE replacing poetry→uv so the + # pattern still matches (after replacement the path no longer contains /poetry) + sed -i "\|^ExecStartPre=.*/poetry .*|d" /lib/systemd/system/cape-fstab.service || true + sed -i "\|^ExecStartPre=.*/poetry .*|d" /lib/systemd/system/cape-rooter.service || true sed -i "s|/etc/poetry/bin/poetry|$PYTHON_MGR|g" /lib/systemd/system/cape*.service sed -i "s|/etc/poetry/bin/poetry|$PYTHON_MGR|g" /lib/systemd/system/guac*.service - # remove poetry config commands as uv does not have them or needs them - sed -i "s|^ExecStartPre=.*/poetry .*||g" /lib/systemd/system/cape-fstab.service || true - sed -i "s|^ExecStartPre=.*/poetry .*||g" /lib/systemd/system/cape-rooter.service || true fi systemctl daemon-reload @@ -1542,13 +1535,8 @@ function install_node_exporter() { function install_volatility3() { echo "[+] Installing volatility3" sudo apt-get install -y unzip - if [ "$USE_UV" = "true" ] || [ "$USE_UV" = "True" ]; then - sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_CMD pip install git+https://github.com/volatilityfoundation/volatility3" - vol_path=$(sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR run python3 -c \"import volatility3.plugins;print(volatility3.__file__.replace('__init__.py', 'symbols/'))\"") - else - sudo -u ${USER} $PYTHON_MGR $PYTHON_MGR_CMD pip3 install git+https://github.com/volatilityfoundation/volatility3 - vol_path=$(sudo -u ${USER} $PYTHON_MGR $PYTHON_MGR_CMD python3 -c "import volatility3.plugins;print(volatility3.__file__.replace('__init__.py', 'symbols/'))") - fi + sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_PIP install git+https://github.com/volatilityfoundation/volatility3" + vol_path=$(sudo -u ${USER} bash -c "cd $CAPE_ROOT && $PYTHON_MGR $PYTHON_MGR_CMD python3 -c \"import volatility3.plugins;print(volatility3.__file__.replace('__init__.py', 'symbols/'))\"") if [ -z "$vol_path" ]; then echo "[-] Could not find volatility3 path" @@ -1636,7 +1624,7 @@ function install_guacamole() { sudo usermod www-data -G ${USER} cd $CAPE_ROOT - sudo -u ${USER} bash -c "export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring; ${poetry_path} $PYTHON_MGR_INSTALL" + sudo -u ${USER} bash -c "cd $CAPE_ROOT && export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring; $PYTHON_MGR $PYTHON_MGR_INSTALL_PYPROJECT" cd .. systemctl daemon-reload @@ -1747,9 +1735,8 @@ case $COMMAND in exit 0;; esac -if [ $# -eq 3 ]; then - sandbox_version=$2 - IFACE_IP=$3 +if [ $# -ge 2 ] && [[ ! "$2" =~ ^-- ]]; then + IFACE_IP=$2 elif [ $# -eq 0 ]; then echo "[-] check --help" exit 1 @@ -1768,14 +1755,10 @@ for i in "$@"; do DISABLE_LIBVIRT=1 elif [ "$i" == "--use-uv" ] || [ "$i" == "USE_UV=true" ] || [ "$i" == "USE_UV=True" ]; then USE_UV="true" - PYTHON_MGR="/usr/local/bin/uv" - PYTHON_MGR_CMD="run" - PYTHON_MGR_INSTALL="" + set_python_mgr fi done -sandbox_version=$(echo "$sandbox_version"|tr "{A-Z}" "{a-z}") - #check if start with root if [ "$EUID" -ne 0 ] && [[ -z "${BUILD_ENV}" ]]; then echo 'This script must be run as root' @@ -1788,8 +1771,8 @@ case "$COMMAND" in install_mongo install_CAPE install_yara - install_systemd install_suricata + install_systemd install_jemalloc if ! crontab -l | grep -q './smtp_sinkhole.sh'; then crontab -l | { cat; echo "@reboot cd $CAPE_ROOT/utils/ && ./smtp_sinkhole.sh 2>/dev/null"; } | crontab - @@ -1811,8 +1794,8 @@ case "$COMMAND" in install_volatility3 install_mongo install_yara - install_systemd install_suricata + install_systemd install_jemalloc install_logrotate install_mitmproxy @@ -1825,7 +1808,7 @@ case "$COMMAND" in fi # Update FLARE CAPA rules once per day if ! crontab -l | grep -q 'community.py -waf -cr'; then - crontab -l | { cat; echo "5 0 */1 * * cd $CAPE_ROOT/utils/ && sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_CMD python3 community.py -waf -cr && sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_CMD pip install -U flare-capa && systemctl restart cape-processor 2>/dev/null"; } | crontab - + crontab -l | { cat; echo "5 0 */1 * * cd $CAPE_ROOT/utils/ && sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_CMD python3 community.py -waf -cr && sudo -u ${USER} $PYTHON_MGR --directory $CAPE_ROOT/ $PYTHON_MGR_PIP install -U flare-capa && systemctl restart cape-processor 2>/dev/null"; } | crontab - fi install_librenms if [ "$clamav_enable" -ge 1 ]; then diff --git a/installer/kvm-qemu.sh b/installer/kvm-qemu.sh index 3f1a5baf73c..f4f7af63908 100755 --- a/installer/kvm-qemu.sh +++ b/installer/kvm-qemu.sh @@ -608,6 +608,18 @@ EOH echo "[+] You should logout and login " fi + _set_libvirt_default_uri +} + +function _set_libvirt_default_uri() { + local rc_file + if [ "$SHELL" = "/bin/zsh" ] || [ "$SHELL" = "/usr/bin/zsh" ]; then + rc_file="$HOME/.zshrc" + else + rc_file="$HOME/.bashrc" + fi + grep -qxF 'export LIBVIRT_DEFAULT_URI=qemu:///system' "$rc_file" 2>/dev/null \ + || echo 'export LIBVIRT_DEFAULT_URI=qemu:///system' >> "$rc_file" } function install_virt_manager() { @@ -688,13 +700,7 @@ function install_virt_manager() { # https://github.com/virt-manager/virt-manager/blob/main/INSTALL.md meson setup build meson install -C build - if [ "$SHELL" = "/bin/zsh" ] || [ "$SHELL" = "/usr/bin/zsh" ] ; then - echo "export LIBVIRT_DEFAULT_URI=qemu:///system" >> "$HOME/.zsh" - # echo "export GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0:$GI_TYPELIB_PATH" >> "$HOME/.zsh" - else - echo "export LIBVIRT_DEFAULT_URI=qemu:///system" >> "$HOME/.bashrc" - # echo "export GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0:$GI_TYPELIB_PATH" >> "$HOME/.bashrc" - fi + _set_libvirt_default_uri if [ -f /usr/share/virt-manager/local/share/glib-2.0/schemas/org.virt-manager.virt-manager.gschema.xml ]; then cp /usr/share/virt-manager/local/share/glib-2.0/schemas/org.virt-manager.virt-manager.gschema.xml /usr/share/glib-2.0/schemas/ diff --git a/lib/cuckoo/common/demux.py b/lib/cuckoo/common/demux.py index 91718c29518..5275b721593 100644 --- a/lib/cuckoo/common/demux.py +++ b/lib/cuckoo/common/demux.py @@ -2,10 +2,11 @@ # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org # See the file 'docs/LICENSE' for copying permission. +import re import logging import os import tempfile -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Optional from lib.cuckoo.common.config import Config from lib.cuckoo.common.exceptions import CuckooDemuxError @@ -123,6 +124,111 @@ ] +IGNORABLE_PATTERNS = ( + re.compile(br"msvcp\d+\.dll$", re.IGNORECASE), + re.compile(br"vcruntime\d+(_\d)?\.dll$", re.IGNORECASE), + re.compile(br"api-ms-win-.*\.dll$", re.IGNORECASE), + re.compile(br"ucrtbase\.dll$", re.IGNORECASE), + re.compile(br"concrt\d+\.dll$", re.IGNORECASE), + re.compile(br"vccorlib\d+\.dll$", re.IGNORECASE), + # You can add more patterns here, e.g., for .NET runtimes: + # re.compile(r"coreclr\.dll$", re.IGNORECASE), + # re.compile(r"System\..*\.dll$", re.IGNORECASE), +) + +EXE_PREFERENCE_LIST = ( + b"setup.exe", + b"install.exe", + b"installer.exe", + b"main.exe", +) + +FILE_EXT_OF_INTEREST = ( + b".bat", + b".cmd", + b".dat", + b".db", + # b".dll", + b".doc", + b".exe", + b".html", + b".js", + b".jse", + b".lnk", + b".msi", + b".ps1", + b".scr", + b".temp", + b".tmp", + b".vbe", + b".vbs", + b".wsf", + b".xls", +) + + +def find_payload_to_run(file_list: List[Any]) -> List[str]: + """ + Analyzes a list of filenames to find the most likely executable payload. + + Args: + file_list: A list of filenames (e.g., from zipfile.namelist()) + + Returns: + A list of filenames of the executables to run. + """ + + executables_found = [] + unknown_other_files = [] + ignorable_files_found = 0 + + for filename in file_list: + # Skip empty names (can happen with directory entries) + if not filename: + continue + + # Ensure filename is bytes to match regexes and endswith accurately + filename_bytes = filename if isinstance(filename, bytes) else filename.encode() + + # --- Step 1: Check against Ignorable Patterns --- + is_ignorable = False + for pattern in IGNORABLE_PATTERNS: + if pattern.match(filename_bytes): + is_ignorable = True + ignorable_files_found += 1 + break + + if is_ignorable: + continue # It's a known runtime DLL, skip to the next file + + # --- Step 2: Check for Executable --- + if filename_bytes.lower().endswith(FILE_EXT_OF_INTEREST): + executables_found.append(filename_bytes) + continue + + # --- Step 3: It's an unknown file --- + unknown_other_files.append(filename_bytes) + + # --- Triage Decision Logic --- + + # Case 1: No executables found at all. + if not executables_found: + log.debug("No executables found. Ignored %d runtime files.", ignorable_files_found) + return [] + + # Case 2: Multiple executables found. Use heuristics. + log.debug("Found multiple executables: %s", str(executables_found)) + + # Check against our preferred list + for preferred_name in EXE_PREFERENCE_LIST: + for exe_name in executables_found: + if exe_name.lower() == preferred_name: + log.debug("Heuristic: Choosing '%s' from preference list.", exe_name) + return [exe_name.decode(errors="ignore")] + + # if no preferred name, return all files of interest + return [exe.decode(errors="ignore") for exe in executables_found] + def options2passwd(options: str) -> str: password = "" if "password=" in options: @@ -220,11 +326,15 @@ def _sf_children(child: Any) -> Tuple[bytes, str, str, int]: return path_to_extract, child.platform, child.magic or "", child.filesize -def demux_sflock(filename: bytes, options: str, check_shellcode: bool = True) -> Tuple[List[Tuple[bytes, str, str, int]], str]: +# ToDo fix typing need to add str as error msg +def demux_sflock( + filename: bytes, options: str, check_shellcode: bool = True +) -> Tuple[List[Tuple[bytes, str, str, int]], str, List[str]]: retlist = [] + submit_opts = [] # do not extract from .bin (downloaded from us) if os.path.splitext(filename)[1] == b".bin": - return retlist, "" + return retlist, "", submit_opts # ToDo need to introduce error msgs here try: @@ -234,7 +344,7 @@ def demux_sflock(filename: bytes, options: str, check_shellcode: bool = True) -> # Before unpacking, ensure the file actually exists and is not empty to avoid IncorrectUsageException if not path_exists(filename) or os.path.getsize(filename) == 0: - return [(filename, platform, magic_type, file_size)], "file not found or empty" + return [(filename, platform, magic_type, file_size)], "file not found or empty", [] password = options2passwd(options) or "infected" try: @@ -247,9 +357,48 @@ def demux_sflock(filename: bytes, options: str, check_shellcode: bool = True) -> magic_type = file.get_type() or "" platform = file.get_platform() file_size = file.get_size() - return [(filename, platform, magic_type, file_size)], "" + return [(filename, platform, magic_type, file_size)], "", [] if unpacked.package in blacklist_extensions: - return [], "blacklisted package" + return retlist, "blacklisted package", submit_opts + + if unpacked.filepaths: + # CASE 1: Archive contains multiple files + if len(unpacked.filepaths) > 1: + # Find interesting files (exe/dll) and submit the ORIGINAL archive + # with instructions to run specifically those files. + execs = find_payload_to_run(unpacked.filepaths) + submit_opts = [f"file={runable}" for runable in execs] + # returning empty retlist so it will use parent file + return [], "", submit_opts + + # CASE 2: Archive contains exactly 1 file + single_file = unpacked.filepaths[0] + # assuming unpacked.children matches unpacked.filepaths indices + # we get the object representing this single file + current_child = unpacked.children[0] if unpacked.children else None + + if current_child: + if single_file.endswith((b".7z", b".rar", b".zip")): + # It's a sub-archive. We need to go one level deeper. + # We loop through THIS sub-archive's children. + # Note: Ensure 'current_child.children' exists in your object model. + # If 'unpacked.children' already contained the deep files, this loop might need adjusting based on your specific API. + execs = find_payload_to_run(getattr(current_child, "filepaths", [])) + if execs: + extracted = _sf_children(current_child) + path = extracted[0] + if path: + submit_opts += [f"file={runable}" for runable in execs] + retlist.append(extracted) + else: + # It's just a single regular file (e.g., malware.exe inside a zip). + # Extract and add to task. + extracted = _sf_children(current_child) + path = extracted[0] + if path: + retlist.append(extracted) + return retlist, "", submit_opts + for sf_child in unpacked.children: if sf_child.to_dict().get("children"): for ch in sf_child.children: @@ -271,7 +420,29 @@ def demux_sflock(filename: bytes, options: str, check_shellcode: bool = True) -> retlist.append(tmp_child) except Exception as e: log.exception(e) - return list(filter(None, retlist)), "" + return list(filter(None, retlist)), "", submit_opts + + +def _prepare_file(filename: bytes, options: str = "", size: int = 0) -> Tuple[Optional[bytes], Optional[str]]: + try: + f_basename = os.path.basename(filename).decode('utf-8', errors='replace') + except Exception: + f_basename = "unknown_filename" + + if not size: + size = File(filename).get_size() + if size <= web_cfg.general.max_sample_size: + return filename, None + + safe_options = options or "" + if web_cfg.general.allow_ignore_size and "ignore_size_check" in safe_options: + return filename, None + + if web_cfg.general.enable_trim and trim_file(filename): + return trimmed_path(filename), None + + error_msg = f"File too big ({f_basename}), enable 'allow_ignore_size' in web.conf or use 'ignore_size_check' option" + return None, error_msg def demux_sample( @@ -297,121 +468,105 @@ def demux_sample( error_list = [] retlist = [] - # if a package was specified, trim if allowed and required + + # --- 1. Quick Handle: Specific Package --- if package: - if package in ("msix",): - retlist.append((filename, "windows")) + if package == "msix": + return [(filename, "windows")], [] + + valid_file, error = _prepare_file(filename, options) + if valid_file: + return [(valid_file, platform)], [] else: - if File(filename).get_size() <= web_cfg.general.max_sample_size or ( - web_cfg.general.allow_ignore_size and "ignore_size_check" in options - ): - retlist.append((filename, platform)) - else: - if web_cfg.general.enable_trim and trim_file(filename): - retlist.append((trimmed_path(filename), platform)) - else: - error_list.append( - { - os.path.basename( - filename - ).decode(): "File too big, enable 'allow_ignore_size' in web.conf or use 'ignore_size_check' option" - } - ) - return retlist, error_list + return [], [{os.path.basename(filename).decode(errors='ignore'): error}] - # handle quarantine files + # --- 2. File Preparation (Unquarantine) --- filename = unquarantine(filename) # don't try to extract from office docs magic = File(filename).get_type() or "" - # if file is an Office doc and password is supplied, try to decrypt the doc - if "Microsoft" in magic: - pass - # ignore = {"Outlook", "Message", "Disk Image"} - elif any(x in magic for x in OFFICE_TYPES): - password = options2passwd(options) or None - if use_sflock: - if HAS_SFLOCK: - retlist = demux_office(filename, password, platform) - return retlist, error_list - else: - log.error("Detected password protected office file, but no sflock is installed: poetry install") - error_list.append( - { - os.path.basename( - filename - ).decode(): "Detected password protected office file, but no sflock is installed or correct password provided" - } - ) - - # don't try to extract from Java archives or executables - if ( - "Java Jar" in magic - or "Java archive data" in magic - or "PE32" in magic - or "MS-DOS executable" in magic - or any(x in magic for x in File.LINUX_TYPES) - ): - retlist = [] - if File(filename).get_size() <= web_cfg.general.max_sample_size or ( - web_cfg.general.allow_ignore_size and "ignore_size_check" in options - ): - retlist.append((filename, platform)) + + # --- 3. Handle Password-Protected Office Files --- + is_office = "Microsoft" in magic or any(x in magic for x in OFFICE_TYPES) + if is_office and use_sflock: + password = options2passwd(options) + if HAS_SFLOCK and password: + retlist = demux_office(filename, password, platform) + return retlist, error_list else: - if web_cfg.general.enable_trim and trim_file(filename): - retlist.append((trimmed_path(filename), platform)) - else: - error_list.append( - { - os.path.basename( - filename - ).decode(): "File too big, enable 'allow_ignore_size' in web.conf or use 'ignore_size_check' option", - } - ) + log.error("Detected password protected office file, but no sflock is installed.") + return [], [{os.path.basename(filename).decode(errors='ignore'): "Detected password protected office file, but no sflock is installed"}] + + # --- 4. Skip Extraction for specific types --- + ignored_signatures = [ + "Java Jar", + "Java archive data", + "PE32", + "MS-DOS executable" + ] + # Añadimos tipos de Linux si están definidos + if hasattr(File, "LINUX_TYPES"): + ignored_signatures.extend(File.LINUX_TYPES) + + # If magic matches an ignored type, treat as regular file and skip sflock + if any(sig in magic for sig in ignored_signatures): + valid_file, error = _prepare_file(filename, options) + if valid_file: + return [(valid_file, platform)], [] + else: + return [], [{os.path.basename(filename).decode(errors='ignore'): error}] + + # --- 5. Generic Extraction (Sflock) --- + check_shellcode = "check_shellcode=0" not in (options or "") + + # Inicializamos variables de retorno de sflock + extracted_files = [] + sflock_error = "" + execs = [] + + if HAS_SFLOCK and use_sflock: + extracted_files, sflock_error, execs = demux_sflock(filename, options, check_shellcode) + + # Si sflock encontró ejecutables específicos que quiere forzar (ej. payload en un zip) + if execs: + for opt in execs: + error_list.append({"option": opt}) + + # If nothing extracted (not an archive, or sflock failed/skipped) + if not extracted_files: + if sflock_error: + error_list.append({os.path.basename(filename).decode(errors='ignore'): sflock_error}) + + # Fallback: submit the original file + valid_file, error = _prepare_file(filename, options) + if valid_file: + retlist.append((valid_file, platform)) + elif error and not sflock_error: + # Only report size error if sflock didn't already report a more relevant error + error_list.append({os.path.basename(filename).decode(errors='ignore'): error}) + return retlist, error_list - new_retlist = [] + # --- 6. Process Extracted Files --- + for block in extracted_files: + if len(retlist) >= demux_files_limit: + break - check_shellcode = True - if options and "check_shellcode=0" in options: - check_shellcode = False + ex_filename, ex_platform, ex_magic, ex_size = block + f_basename_str = os.path.basename(ex_filename).decode(errors='ignore') - # all in one unarchiver - retlist, error_msg = demux_sflock(filename, options, check_shellcode) if HAS_SFLOCK and use_sflock else ([], "") - # if it isn't a ZIP or an email, or we aren't able to obtain anything interesting from either, then just submit the - # original file - if not retlist: - if error_msg: - error_list.append({os.path.basename(filename).decode(): error_msg}) - new_retlist.append((filename, platform)) - else: - for entry in retlist: - if not isinstance(entry, (list, tuple)) or len(entry) < 2: - log.warning("Skipping invalid entry in retlist: %s", entry) - continue - filename, platform = entry[0], entry[1] - magic_type = entry[2] if len(entry) > 2 else "" - file_size = entry[3] if len(entry) > 3 else 0 - # verify not Windows binaries here: - if platform == "linux" and not linux_enabled and "Python" not in magic_type: - error_list.append({os.path.basename(filename).decode(): "Linux processing is disabled"}) - continue - - if file_size > web_cfg.general.max_sample_size: - if web_cfg.general.allow_ignore_size and "ignore_size_check" in options: - if web_cfg.general.enable_trim: - # maybe identify here - if trim_file(filename): - filename = trimmed_path(filename) - else: - error_list.append( - { - os.path.basename( - filename - ).decode(): "File too big, enable 'allow_ignore_size' in web.conf or use 'ignore_size_check' option", - } - ) - new_retlist.append((filename, platform)) + # Platform check (Linux) + if ex_platform == "linux" and not linux_enabled and "Python" not in ex_magic: + error_list.append({f_basename_str: "Linux processing is disabled"}) + continue - return new_retlist[:demux_files_limit], error_list + # Validate size and trim if necessary + # Pass 'ex_size' to _prepare_file to avoid re-calculating it + valid_file, error = _prepare_file(ex_filename, options, size=ex_size) + + if valid_file: + retlist.append((valid_file, ex_platform)) + else: + error_list.append({f_basename_str: error}) + return retlist, error_list diff --git a/lib/cuckoo/core/analysis_manager.py b/lib/cuckoo/core/analysis_manager.py index b7f406295e6..0dc4fd428f9 100644 --- a/lib/cuckoo/core/analysis_manager.py +++ b/lib/cuckoo/core/analysis_manager.py @@ -22,7 +22,7 @@ from lib.cuckoo.common.path_utils import path_delete, path_exists, path_mkdir from lib.cuckoo.common.utils import convert_to_printable, create_folder, get_memdump_path from lib.cuckoo.core.database import Database, _Database -from lib.cuckoo.core.data.task import TASK_COMPLETED, TASK_PENDING, TASK_RUNNING, Task +from lib.cuckoo.core.data.task import TASK_COMPLETED, TASK_PENDING, TASK_RUNNING, TASK_FAILED_ANALYSIS, Task from lib.cuckoo.core.data.machines import Machine from lib.cuckoo.core.data.guests import Guest from lib.cuckoo.core.guest import GuestManager @@ -309,6 +309,7 @@ def category_checks(self) -> Optional[bool]: def machine_running(self) -> Generator[None, None, None]: assert self.machinery_manager and self.machine and self.guest + is_dead = False try: with self.db.session.begin(): self.machinery_manager.start_machine(self.machine) @@ -319,6 +320,7 @@ def machine_running(self) -> Generator[None, None, None]: self.dump_machine_memory() except (CuckooMachineError, CuckooGuestCriticalTimeout) as e: + is_dead = True # This machine has turned dead, so we'll throw an exception # which informs the AnalysisManager that it should analyze # this task again with another available machine. @@ -337,25 +339,24 @@ def machine_running(self) -> Generator[None, None, None]: shutil.rmtree(self.storage) raise CuckooDeadMachine(self.machine.name) from e + finally: + if not is_dead: + try: + with self.db.session.begin(): + self.machinery_manager.stop_machine(self.machine) + except CuckooMachineError as e: + self.log.warning("Unable to stop machine %s: %s", self.machine.label, e) - with self.db.session.begin(): - try: - self.machinery_manager.stop_machine(self.machine) - except CuckooMachineError as e: - self.log.warning("Unable to stop machine %s: %s", self.machine.label, e) - # Explicitly rollback since we don't re-raise the exception. - self.db.session.rollback() - - try: - # Release the analysis machine, but only if the machine is not dead. - with self.db.session.begin(): - self.machinery_manager.machinery.release(self.machine) - except CuckooMachineError as e: - self.log.error( - "Unable to release machine %s, reason %s. You might need to restore it manually", - self.machine.label, - e, - ) + try: + # Release the analysis machine, but only if the machine is not dead. + with self.db.session.begin(): + self.machinery_manager.machinery.release(self.machine) + except CuckooMachineError as e: + self.log.error( + "Unable to release machine %s, reason %s. You might need to restore it manually", + self.machine.label, + e, + ) def dump_machine_memory(self) -> None: if not self.cfg.cuckoo.memory_dump and not self.task.memory: @@ -482,6 +483,13 @@ def launch_analysis(self) -> None: # Put the task back in pending so that the schedule can attempt to choose a new machine. self.db.set_status(self.task.id, TASK_PENDING) raise + except Exception as e: + self.log.exception("Unexpected exception during analysis: %s", e) + with self.db.session.begin(): + self.db.set_status(self.task.id, TASK_FAILED_ANALYSIS) + if hasattr(self, "machine") and self.machine: + self.db.unlock_machine(self.machine) + raise else: with self.db.session.begin(): self.db.set_status(self.task.id, TASK_COMPLETED) diff --git a/lib/cuckoo/core/guest.py b/lib/cuckoo/core/guest.py index 2a3de974af1..15e3c446cfa 100644 --- a/lib/cuckoo/core/guest.py +++ b/lib/cuckoo/core/guest.py @@ -308,6 +308,14 @@ def start_analysis(self, options): # Upload the analyzer. self.upload_analyzer() + # Update file_name in options if category is file/archive to include task-id unique subdirectory + # This must be done BEFORE self.add_config(options) is called so that analysis.conf in guest has the correct path + if options["category"] in ("file", "archive"): + if self.platform == "windows": + options["file_name"] = f"{options['id']}\\{sanitize_filename(options['file_name'])}" + else: + options["file_name"] = f"{options['id']}/{sanitize_filename(options['file_name'])}" + # Pass along the analysis.conf file. self.add_config(options) # Allow Auxiliary modules to prepare the Guest. @@ -328,9 +336,9 @@ def start_analysis(self, options): if options["category"] in ("file", "archive"): # Use the correct os.sep in the filepath based on what OS this file is destined for if self.platform == "windows": - filepath = ntpath.join(self.determine_temp_path(), sanitize_filename(options["file_name"])) + filepath = ntpath.join(self.determine_temp_path(), options["file_name"]) else: - filepath = os.path.join(self.determine_temp_path(), sanitize_filename(options["file_name"])) + filepath = os.path.join(self.determine_temp_path(), options["file_name"]) data = {"filepath": filepath} files = { "file": ("sample.bin", open(sample_path, "rb")), diff --git a/pyproject.toml b/pyproject.toml index 6308e2565c5..ec851f90306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,9 @@ dev = [ [tool.poetry] package-mode = false +[tool.uv] +package = false + [tool.black] line-length = 132 include = "\\.py(_disabled)?$" diff --git a/requirements.txt b/requirements.txt index df039da0189..23603b775bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,127 +1,127 @@ aiohappyeyeballs==2.6.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 -aiohttp==3.13.4 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144 \ - --hash=sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9 \ - --hash=sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed \ - --hash=sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182 \ - --hash=sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576 \ - --hash=sha256:0d0dbc6c76befa76865373d6aa303e480bb8c3486e7763530f7f6e527b471118 \ - --hash=sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965 \ - --hash=sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e \ - --hash=sha256:10fb7b53262cf4144a083c9db0d2b4d22823d6708270a9970c4627b248c6064c \ - --hash=sha256:13168f5645d9045522c6cef818f54295376257ed8d02513a37c2ef3046fc7a97 \ - --hash=sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e \ - --hash=sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933 \ - --hash=sha256:1746338dc2a33cf706cd7446575d13d451f28f9860bebc908c7632b22e71ae3f \ - --hash=sha256:1867087e2c1963db1216aedf001efe3b129835ed2b05d97d058176a6d08b5726 \ - --hash=sha256:19f60011ad60e40a01d242238bb335399e3a4d8df958c63cbb835add8d5c3b5a \ - --hash=sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5 \ - --hash=sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871 \ - --hash=sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758 \ - --hash=sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0 \ - --hash=sha256:26ed03f7d3d6453634729e2c7600d7255d65e879559c5a48fe1bb78355cde74b \ - --hash=sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7 \ - --hash=sha256:2d15e7e4f1099d9e4d863eaf77a8eee5dcb002b7d7188061b0fbee37f845899e \ - --hash=sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e \ - --hash=sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f \ - --hash=sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d \ - --hash=sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927 \ - --hash=sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7 \ - --hash=sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3 \ - --hash=sha256:383880f7b8de5ac208fa829c7038d08e66377283b2de9e791b71e06e803153c2 \ - --hash=sha256:3b4e07d8803a70dd886b5f38588e5b49f894995ca8e132b06c31a2583ae2ef6e \ - --hash=sha256:3cdd3393130bf6588962441ffd5bde1d3ea2d63a64afa7119b3f3ba349cebbe7 \ - --hash=sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9 \ - --hash=sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329 \ - --hash=sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1 \ - --hash=sha256:463fa18a95c5a635d2b8c09babe240f9d7dbf2a2010a6c0b35d8c4dff2a0e819 \ - --hash=sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42 \ - --hash=sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70 \ - --hash=sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7 \ - --hash=sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9 \ - --hash=sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2 \ - --hash=sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c \ - --hash=sha256:4c3f733916e85506b8000dddc071c6b82f8c68f56c99adb328d6550017db062d \ - --hash=sha256:4e2e68085730a03704beb2cff035fa8648f62c9f93758d7e6d70add7f7bb5b3b \ - --hash=sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a \ - --hash=sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3 \ - --hash=sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e \ - --hash=sha256:5539ec0d6a3a5c6799b661b7e79166ad1b7ae71ccb59a92fcb6b4ef89295bc94 \ - --hash=sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2 \ - --hash=sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d \ - --hash=sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be \ - --hash=sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77 \ - --hash=sha256:6234bf416a38d687c3ab7f79934d7fb2a42117a5b9813aca07de0a5398489023 \ - --hash=sha256:6290fe12fe8cefa6ea3c1c5b969d32c010dfe191d4392ff9b599a3f473cbe722 \ - --hash=sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538 \ - --hash=sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453 \ - --hash=sha256:6b335919ffbaf98df8ff3c74f7a6decb8775882632952fd1810a017e38f15aee \ - --hash=sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f \ - --hash=sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b \ - --hash=sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7 \ - --hash=sha256:717d17347567ded1e273aa09918650dfd6fd06f461549204570c7973537d4123 \ - --hash=sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e \ - --hash=sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3 \ - --hash=sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c \ - --hash=sha256:7520d92c0e8fbbe63f36f20a5762db349ff574ad38ad7bc7732558a650439845 \ - --hash=sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a \ - --hash=sha256:797613182ffaaca0b9ad5f3b3d3ce5d21242c768f75e66c750b8292bd97c9de3 \ - --hash=sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763 \ - --hash=sha256:7c65738ac5ae32b8feef699a4ed0dc91a0c8618b347781b7461458bbcaaac7eb \ - --hash=sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c \ - --hash=sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83 \ - --hash=sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8 \ - --hash=sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942 \ - --hash=sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab \ - --hash=sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1 \ - --hash=sha256:907ad36b6a65cff7d88d7aca0f77c650546ba850a4f92c92ecb83590d4613249 \ - --hash=sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073 \ - --hash=sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde \ - --hash=sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27 \ - --hash=sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c \ - --hash=sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500 \ - --hash=sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069 \ - --hash=sha256:a5444dce2e6fba0a1dc2d58d026e674f25f21de178c6f844342629bcef019f2f \ - --hash=sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9 \ - --hash=sha256:a7058af1f53209fdf07745579ced525d38d481650a989b7aa4a3b484b901cdab \ - --hash=sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d \ - --hash=sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21 \ - --hash=sha256:b3d525648fe7c8b4977e460c18098f9f81d7991d72edfdc2f13cf96068f279bc \ - --hash=sha256:b3f00bb9403728b08eb3951e982ca0a409c7a871d709684623daeab79465b181 \ - --hash=sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8 \ - --hash=sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb \ - --hash=sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3 \ - --hash=sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145 \ - --hash=sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d \ - --hash=sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165 \ - --hash=sha256:c344c47e85678e410b064fc2ace14db86bb69db7ed5520c234bf13aed603ec30 \ - --hash=sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8 \ - --hash=sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30 \ - --hash=sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954 \ - --hash=sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7 \ - --hash=sha256:cb15595eb52870f84248d7cc97013a76f52ab02ff74d394be093b1d9b8b82bc0 \ - --hash=sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba \ - --hash=sha256:ce7320a945aac4bf0bb8901600e4f9409eb602f25ce3ef4d275b48f6d704a862 \ - --hash=sha256:d2710ae1e1b81d0f187883b6e9d66cecf8794b50e91aa1e73fc78bfb5503b5d9 \ - --hash=sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349 \ - --hash=sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393 \ - --hash=sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97 \ - --hash=sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12 \ - --hash=sha256:d904084985ca66459e93797e5e05985c048a9c0633655331144c089943e53d12 \ - --hash=sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38 \ - --hash=sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b \ - --hash=sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551 \ - --hash=sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57 \ - --hash=sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c \ - --hash=sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb \ - --hash=sha256:eb10ce8c03850e77f4d9518961c227be569e12f71525a7e90d17bca04299921d \ - --hash=sha256:ec75fc18cb9f4aca51c2cbace20cf6716e36850f44189644d2d69a875d5e0532 \ - --hash=sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360 \ - --hash=sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f \ - --hash=sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c \ - --hash=sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791 +aiohttp==3.13.2 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329 \ + --hash=sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6 \ + --hash=sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7 \ + --hash=sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254 \ + --hash=sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742 \ + --hash=sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad \ + --hash=sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906 \ + --hash=sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5 \ + --hash=sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811 \ + --hash=sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded \ + --hash=sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca \ + --hash=sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f \ + --hash=sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98 \ + --hash=sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a \ + --hash=sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a \ + --hash=sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155 \ + --hash=sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6 \ + --hash=sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e \ + --hash=sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b \ + --hash=sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e \ + --hash=sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b \ + --hash=sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa \ + --hash=sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf \ + --hash=sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5 \ + --hash=sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc \ + --hash=sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514 \ + --hash=sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f \ + --hash=sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07 \ + --hash=sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca \ + --hash=sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e \ + --hash=sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9 \ + --hash=sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0 \ + --hash=sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782 \ + --hash=sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d \ + --hash=sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213 \ + --hash=sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293 \ + --hash=sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead \ + --hash=sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04 \ + --hash=sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61 \ + --hash=sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3 \ + --hash=sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4 \ + --hash=sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b \ + --hash=sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da \ + --hash=sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6 \ + --hash=sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22 \ + --hash=sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725 \ + --hash=sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be \ + --hash=sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661 \ + --hash=sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e \ + --hash=sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d \ + --hash=sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989 \ + --hash=sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c \ + --hash=sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec \ + --hash=sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45 \ + --hash=sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a \ + --hash=sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8 \ + --hash=sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac \ + --hash=sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694 \ + --hash=sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636 \ + --hash=sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169 \ + --hash=sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a \ + --hash=sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204 \ + --hash=sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a \ + --hash=sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab \ + --hash=sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae \ + --hash=sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948 \ + --hash=sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8 \ + --hash=sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5 \ + --hash=sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30 \ + --hash=sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16 \ + --hash=sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693 \ + --hash=sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084 \ + --hash=sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3 \ + --hash=sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4 \ + --hash=sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592 \ + --hash=sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49 \ + --hash=sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a \ + --hash=sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e \ + --hash=sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4 \ + --hash=sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9 \ + --hash=sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b \ + --hash=sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f \ + --hash=sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802 \ + --hash=sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb \ + --hash=sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b \ + --hash=sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011 \ + --hash=sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3 \ + --hash=sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7 \ + --hash=sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c \ + --hash=sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734 \ + --hash=sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700 \ + --hash=sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d \ + --hash=sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901 \ + --hash=sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2 \ + --hash=sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5 \ + --hash=sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23 \ + --hash=sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8 \ + --hash=sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613 \ + --hash=sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb \ + --hash=sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251 \ + --hash=sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6 \ + --hash=sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780 \ + --hash=sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd \ + --hash=sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c \ + --hash=sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf \ + --hash=sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa \ + --hash=sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40 \ + --hash=sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7 \ + --hash=sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673 \ + --hash=sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb \ + --hash=sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61 \ + --hash=sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940 \ + --hash=sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be \ + --hash=sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b \ + --hash=sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4 \ + --hash=sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476 \ + --hash=sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d \ + --hash=sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f \ + --hash=sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248 \ + --hash=sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a aiosignal==1.4.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 @@ -388,56 +388,38 @@ crispy-bootstrap4==2024.10 ; python_version >= "3.10" and python_version < "4.0" crudini==0.9.5 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:59ae650f45af82a64afc33eb876909ee0c4888dc4e8711ef59731c1edfda5e24 \ --hash=sha256:84bc208dc7d89571bdc3c99274259d0b32d6b3a692d4255524f2eb4b64e9195c -cryptography==46.0.7 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ - --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ - --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ - --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ - --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ - --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ - --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ - --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ - --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ - --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ - --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ - --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ - --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ - --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ - --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ - --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ - --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ - --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ - --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ - --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ - --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ - --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ - --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ - --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ - --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ - --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ - --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ - --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ - --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ - --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ - --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ - --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ - --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ - --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ - --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ - --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ - --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ - --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ - --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ - --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ - --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ - --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ - --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ - --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ - --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ - --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ - --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ - --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ - --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce +cryptography==44.0.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7 \ + --hash=sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3 \ + --hash=sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183 \ + --hash=sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69 \ + --hash=sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a \ + --hash=sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62 \ + --hash=sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911 \ + --hash=sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7 \ + --hash=sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a \ + --hash=sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41 \ + --hash=sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83 \ + --hash=sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12 \ + --hash=sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864 \ + --hash=sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf \ + --hash=sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c \ + --hash=sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2 \ + --hash=sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b \ + --hash=sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0 \ + --hash=sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4 \ + --hash=sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9 \ + --hash=sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008 \ + --hash=sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862 \ + --hash=sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009 \ + --hash=sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7 \ + --hash=sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f \ + --hash=sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026 \ + --hash=sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f \ + --hash=sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd \ + --hash=sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420 \ + --hash=sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14 \ + --hash=sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00 cxxfilt==0.3.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:774e85a8d0157775ed43276d89397d924b104135762d86b3a95f81f203094e07 \ --hash=sha256:7df6464ba5e8efbf0d8974c0b2c78b32546676f06059a83515dbdfa559b34214 @@ -534,9 +516,9 @@ django-recaptcha==4.0.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:5316438f97700c431d65351470d1255047e3f2cd9af0f2f13592b637dad9213e django-settings-export==1.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:fceeae49fc597f654c1217415d8e049fc81c930b7154f5d8f28c432db738ff79 -django==5.1.15 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432 \ - --hash=sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947 +django==5.1.14 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:2a4b9c20404fd1bf50aaaa5542a19d860594cba1354f688f642feb271b91df27 \ + --hash=sha256:b98409fb31fdd6e8c3a6ba2eef3415cc5c0020057b43b21ba7af6eff5f014831 djangorestframework==3.15.2 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20 \ --hash=sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad @@ -890,9 +872,9 @@ ida-settings==3.3.0 ; python_version >= "3.10" and python_version < "4.0" \ idapro==0.0.7 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:0b2e184b19e4d8404ea48b1f17f39b21b054931315940ee9716ef8559144988f \ --hash=sha256:3cb8d48ac21a19e6a0ef65eacd0005a1f9c40a264a7bcb675914eaa20c6e1371 -idna==3.15 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.10 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 incremental==24.7.2 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe \ --hash=sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9 @@ -910,147 +892,151 @@ jsbeautifier==1.15.1 ; python_version >= "3.10" and python_version < "4.0" \ lnkparse3==1.5.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:3ecbd8f4107be07b8e8d7b770daa53271abf66222ee892618d30f86952e1121a \ --hash=sha256:56b549389254f4d25375621249aa3a8c31f1dabf375e88bf7dc8c73a0f4f8f1e -lxml==6.1.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2 \ - --hash=sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773 \ - --hash=sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7 \ - --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ - --hash=sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635 \ - --hash=sha256:08950a23f296b3f83521577274e3d3b0f3d739bf2e68d01a752e4288bc50d286 \ - --hash=sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9 \ - --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ - --hash=sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c \ - --hash=sha256:11a873c77a181b4fef9c2e357d08ed399542c2af1390101da66720a19c7c9618 \ - --hash=sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7 \ - --hash=sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39 \ - --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ - --hash=sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551 \ - --hash=sha256:1dd6a1c3ad4cb674f44525d9957f3e9c209bb6dd9213245195167a281fcc2bdc \ - --hash=sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd \ - --hash=sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd \ - --hash=sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405 \ - --hash=sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289 \ - --hash=sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b \ - --hash=sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a \ - --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ - --hash=sha256:26c5272c6a4bf4cf32d3f5a7890c942b0e04438691157d341616d02cca74d4bd \ - --hash=sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac \ - --hash=sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62 \ - --hash=sha256:29f5c00cb7d752bce2c70ebd2d31b0a42f9499ffdd3ecb2f31a5b73ee43031ad \ - --hash=sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f \ - --hash=sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad \ - --hash=sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c \ - --hash=sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292 \ - --hash=sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544 \ - --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ - --hash=sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120 \ - --hash=sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512 \ - --hash=sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb \ - --hash=sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32 \ - --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec \ - --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ - --hash=sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43 \ - --hash=sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037 \ - --hash=sha256:468479e52ecf3ec23799c863336d02c05fc2f7ffd1a1424eeeb9a28d4eb69d13 \ - --hash=sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03 \ - --hash=sha256:481d6e2104285d9add34f41b42b247b76b61c5b5c26c303c2e9707bbf8bd9a64 \ - --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ - --hash=sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 \ - --hash=sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb \ - --hash=sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9 \ - --hash=sha256:4e2c54d6b47361d0f1d3bc8d4e082ad87201e56ccdcca4d3b9ee3644ff595ec8 \ - --hash=sha256:52b0ac6903cf74ebf997eb8c682d2fbac7d1ab7e4c552413eec55868a9b73f39 \ - --hash=sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842 \ - --hash=sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f \ - --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ - --hash=sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c \ - --hash=sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7 \ - --hash=sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6 \ - --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ - --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ - --hash=sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9 \ - --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb \ - --hash=sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f \ - --hash=sha256:76b958b4ea3104483c20f74866d55aa056546e15ebe83dd7aecd63698f43b755 \ - --hash=sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb \ - --hash=sha256:7ba11752e346bd804ea312ec2eea2532dfa8b8d3261d81a32ef9e6ab16256280 \ - --hash=sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace \ - --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ - --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5 \ - --hash=sha256:81ff55c70b67d19d52b6fd118a114c0a4c97d799cd3089ff9bd9e2ff4b414ee2 \ - --hash=sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3 \ - --hash=sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585 \ - --hash=sha256:8c11b984b5ce6add4dccc7144c7be5d364d298f15b0c6a57da1991baedc750ce \ - --hash=sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946 \ - --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ - --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ - --hash=sha256:920354904d1cb86577d4b3cfe2830c2dbe81d6f4449e57ada428f1609b5985f7 \ - --hash=sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5 \ - --hash=sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28 \ - --hash=sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4 \ - --hash=sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c \ - --hash=sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50 \ - --hash=sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9 \ - --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ - --hash=sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16 \ - --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ - --hash=sha256:a02ca8fe48815bddcfca3248efe54451abb9dbf2f7d1c5744c8aa4142d476919 \ - --hash=sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4 \ - --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ - --hash=sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec \ - --hash=sha256:a86f06f059e22a0d574990ee2df24ede03f7f3c68c1336293eee9536c4c776cd \ - --hash=sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8 \ - --hash=sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f \ - --hash=sha256:b6c2f225662bc5ad416bdd06f72ca301b31b39ce4261f0e0097017fc2891b940 \ - --hash=sha256:bb40648d96157f9081886defe13eac99253e663be969ff938a9289eff6e47b72 \ - --hash=sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366 \ - --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ - --hash=sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690 \ - --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a \ - --hash=sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819 \ - --hash=sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c \ - --hash=sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86 \ - --hash=sha256:c53fa3a5a52122d590e847a57ccf955557b9634a7f99ff5a35131321b0a85317 \ - --hash=sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180 \ - --hash=sha256:c748ebcb6877de89f48ab90ca96642ac458fff5dec291a2b9337cd4d0934e383 \ - --hash=sha256:c871299c595ee004d186f61840f0bfc4941aa3f17c8ba4a565ead7e4f4f820ee \ - --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ - --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ - --hash=sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 \ - --hash=sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e \ - --hash=sha256:d0d799ff958655781296ec870d5e2448e75150da2b3d07f13ff5b0c2c35beefd \ - --hash=sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3 \ - --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:d3829a6e6fd550a219564912d4002c537f65da4c6ae4e093cc34462f4fa027ad \ - --hash=sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea \ - --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ - --hash=sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d \ - --hash=sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de \ - --hash=sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1 \ - --hash=sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d \ - --hash=sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11 \ - --hash=sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9 \ - --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776 \ - --hash=sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f \ - --hash=sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93 \ - --hash=sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a \ - --hash=sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5 \ - --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ - --hash=sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d \ - --hash=sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3 \ - --hash=sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086 \ - --hash=sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215 +lxml==5.3.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e \ + --hash=sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229 \ + --hash=sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3 \ + --hash=sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5 \ + --hash=sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70 \ + --hash=sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15 \ + --hash=sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002 \ + --hash=sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd \ + --hash=sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22 \ + --hash=sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf \ + --hash=sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22 \ + --hash=sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832 \ + --hash=sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727 \ + --hash=sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e \ + --hash=sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30 \ + --hash=sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f \ + --hash=sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f \ + --hash=sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51 \ + --hash=sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4 \ + --hash=sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de \ + --hash=sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875 \ + --hash=sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42 \ + --hash=sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e \ + --hash=sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6 \ + --hash=sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391 \ + --hash=sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc \ + --hash=sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b \ + --hash=sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237 \ + --hash=sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4 \ + --hash=sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86 \ + --hash=sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f \ + --hash=sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a \ + --hash=sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8 \ + --hash=sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f \ + --hash=sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903 \ + --hash=sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03 \ + --hash=sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e \ + --hash=sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99 \ + --hash=sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7 \ + --hash=sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab \ + --hash=sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d \ + --hash=sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22 \ + --hash=sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492 \ + --hash=sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b \ + --hash=sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3 \ + --hash=sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be \ + --hash=sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469 \ + --hash=sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f \ + --hash=sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a \ + --hash=sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c \ + --hash=sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a \ + --hash=sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4 \ + --hash=sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94 \ + --hash=sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442 \ + --hash=sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b \ + --hash=sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84 \ + --hash=sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c \ + --hash=sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9 \ + --hash=sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1 \ + --hash=sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be \ + --hash=sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367 \ + --hash=sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e \ + --hash=sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21 \ + --hash=sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa \ + --hash=sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16 \ + --hash=sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d \ + --hash=sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe \ + --hash=sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83 \ + --hash=sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba \ + --hash=sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040 \ + --hash=sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763 \ + --hash=sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8 \ + --hash=sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff \ + --hash=sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2 \ + --hash=sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a \ + --hash=sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b \ + --hash=sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce \ + --hash=sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c \ + --hash=sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577 \ + --hash=sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8 \ + --hash=sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71 \ + --hash=sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512 \ + --hash=sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540 \ + --hash=sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f \ + --hash=sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2 \ + --hash=sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a \ + --hash=sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce \ + --hash=sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e \ + --hash=sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2 \ + --hash=sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27 \ + --hash=sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1 \ + --hash=sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d \ + --hash=sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1 \ + --hash=sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330 \ + --hash=sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920 \ + --hash=sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99 \ + --hash=sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff \ + --hash=sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18 \ + --hash=sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff \ + --hash=sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c \ + --hash=sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179 \ + --hash=sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080 \ + --hash=sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19 \ + --hash=sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d \ + --hash=sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70 \ + --hash=sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32 \ + --hash=sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a \ + --hash=sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2 \ + --hash=sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79 \ + --hash=sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3 \ + --hash=sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5 \ + --hash=sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f \ + --hash=sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d \ + --hash=sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3 \ + --hash=sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b \ + --hash=sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753 \ + --hash=sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9 \ + --hash=sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957 \ + --hash=sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033 \ + --hash=sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb \ + --hash=sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656 \ + --hash=sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab \ + --hash=sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b \ + --hash=sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d \ + --hash=sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd \ + --hash=sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859 \ + --hash=sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11 \ + --hash=sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c \ + --hash=sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a \ + --hash=sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005 \ + --hash=sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654 \ + --hash=sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80 \ + --hash=sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e \ + --hash=sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec \ + --hash=sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7 \ + --hash=sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965 \ + --hash=sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945 \ + --hash=sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8 maco==1.1.8 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:ab2d1d8e846c0abc455d16f718ba71dda5492ddc22533484156090aa4439fb06 \ --hash=sha256:e0985efdf645d3c55e3d4d4f2bf40b8d2260fa4add608bb8e8fdefba0500cb4a -mako==1.3.12 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ - --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a +mako==1.3.8 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627 \ + --hash=sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8 markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb @@ -1552,101 +1538,81 @@ peepdf-3==5.0.0 ; python_version >= "3.10" and python_version < "4.0" \ pefile==2024.8.26 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f -pillow==12.2.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ - --hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \ - --hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \ - --hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \ - --hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \ - --hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \ - --hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \ - --hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \ - --hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \ - --hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \ - --hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \ - --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \ - --hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \ - --hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \ - --hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \ - --hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \ - --hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \ - --hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \ - --hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \ - --hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \ - --hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \ - --hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \ - --hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \ - --hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \ - --hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \ - --hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \ - --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \ - --hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \ - --hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \ - --hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \ - --hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \ - --hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \ - --hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \ - --hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \ - --hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \ - --hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \ - --hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \ - --hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \ - --hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \ - --hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \ - --hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \ - --hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \ - --hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \ - --hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \ - --hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \ - --hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \ - --hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \ - --hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \ - --hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \ - --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \ - --hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \ - --hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \ - --hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \ - --hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \ - --hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \ - --hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \ - --hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \ - --hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \ - --hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \ - --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \ - --hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \ - --hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \ - --hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \ - --hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \ - --hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \ - --hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \ - --hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \ - --hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \ - --hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \ - --hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \ - --hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \ - --hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \ - --hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \ - --hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \ - --hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \ - --hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \ - --hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \ - --hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \ - --hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \ - --hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \ - --hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \ - --hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \ - --hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \ - --hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \ - --hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \ - --hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \ - --hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \ - --hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \ - --hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \ - --hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \ - --hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5 -pip==26.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1 \ - --hash=sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3 +pillow==11.1.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83 \ + --hash=sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96 \ + --hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \ + --hash=sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a \ + --hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \ + --hash=sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f \ + --hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \ + --hash=sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c \ + --hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \ + --hash=sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49 \ + --hash=sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91 \ + --hash=sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0 \ + --hash=sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2 \ + --hash=sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5 \ + --hash=sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884 \ + --hash=sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e \ + --hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \ + --hash=sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196 \ + --hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \ + --hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \ + --hash=sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269 \ + --hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \ + --hash=sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb \ + --hash=sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a \ + --hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \ + --hash=sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1 \ + --hash=sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8 \ + --hash=sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90 \ + --hash=sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc \ + --hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \ + --hash=sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1 \ + --hash=sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3 \ + --hash=sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35 \ + --hash=sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f \ + --hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \ + --hash=sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2 \ + --hash=sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2 \ + --hash=sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf \ + --hash=sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65 \ + --hash=sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b \ + --hash=sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442 \ + --hash=sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2 \ + --hash=sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade \ + --hash=sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482 \ + --hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \ + --hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \ + --hash=sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a \ + --hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \ + --hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \ + --hash=sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a \ + --hash=sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07 \ + --hash=sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6 \ + --hash=sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f \ + --hash=sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e \ + --hash=sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192 \ + --hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \ + --hash=sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6 \ + --hash=sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73 \ + --hash=sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f \ + --hash=sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6 \ + --hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \ + --hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \ + --hash=sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457 \ + --hash=sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8 \ + --hash=sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26 \ + --hash=sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5 \ + --hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \ + --hash=sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070 \ + --hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \ + --hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \ + --hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761 +pip==25.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \ + --hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd platformdirs==4.3.6 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb @@ -1883,9 +1849,9 @@ psycopg2-binary==2.9.10 ; python_version >= "3.10" and python_version < "4.0" \ pyasn1-modules==0.3.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d -pyasn1==0.6.3 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \ - --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde +pyasn1==0.5.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58 \ + --hash=sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc @@ -2080,9 +2046,9 @@ pyelftools==0.31 ; python_version >= "3.10" and python_version < "4.0" \ pygal==2.4.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:27abab93cbc31e21f3c6bdecc05bda6cd3570cbdbd8297b7caa6904051b50d72 \ --hash=sha256:9204f05380b02a8a32f9bf99d310b51aa2a932cba5b369f7a4dc3705f0a4ce83 -pygments==2.20.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ - --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 +pygments==2.19.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \ + --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c pyguacamole==0.11 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:7f8d8652ce2e86473d72a50e0c9d8a8e0c3c74e373c6b926ca4c851774cae608 \ --hash=sha256:d6facde097a1b1a3048b20fb2ff88b024744ceb2865fb912525da7ebb7779695 @@ -2146,15 +2112,15 @@ pynacl==1.5.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394 \ --hash=sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b \ --hash=sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543 -pyopenssl==26.0.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81 \ - --hash=sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc +pyopenssl==25.0.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90 \ + --hash=sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16 pyparsing==3.2.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1 \ --hash=sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a -pypdf==6.10.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:7d09ce108eff6bf67465d461b6ef352dcb8d84f7a91befc02f904455c6eea11d \ - --hash=sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa +pypdf==5.2.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:7c38e68420f038f2c4998fd9d6717b6db4f6cef1642e9cf384d519c9cf094663 \ + --hash=sha256:d107962ec45e65e3bd10c1d9242bdbbedaa38193c9e3a6617bd6d996e5747b19 pyre2==0.3.10 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:2771bc0a3a5f3fd1d34fe8ae80debd1fe59a1cdcecdbeb60818bff8deb247e56 \ --hash=sha256:29312fbb22b7d3cf3e522c43267098162e98ec8b4fc202c9260e22df671b42e1 \ @@ -2180,9 +2146,9 @@ pysocks==1.7.1 ; python_version >= "3.10" and python_version < "4.0" \ python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 -python-dotenv==1.2.2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ - --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 +python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a python-flirt==0.9.2 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:113c0d865380117bbb5f52870b1459f9ee8fe8f6c5317d2206357c0834a2e9f3 \ --hash=sha256:191f325a137339db07b83112a3a1117d4a8db2c1d17c37bbc7b9078c3fc032c7 \ @@ -2304,9 +2270,9 @@ pyyaml==6.0.2 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 -pyzipper==0.4.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:a4b96afcac04c5589d5abdc6158dd362166374e3cc6810aa441e65f8a17cb9e3 \ - --hash=sha256:aa7b8a0fe741d67aac36ead85f6e735af107b72f84e0775f2ed565fc0d3a2f02 +pyzipper==0.3.6 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc \ + --hash=sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 questionary==2.1.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d \ --hash=sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59 @@ -2555,9 +2521,9 @@ sqlalchemy==2.0.41 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd \ --hash=sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9 \ --hash=sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6 -sqlparse==0.5.4 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e \ - --hash=sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb +sqlparse==0.5.3 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272 \ + --hash=sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca storage3==0.12.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:32ea8f5eb2f7185c2114a4f6ae66d577722e32503f0a30b56e7ed5c7f13e6b48 \ --hash=sha256:9da77fd4f406b019fdcba201e9916aefbf615ef87f551253ce427d8136459a34 @@ -2633,9 +2599,9 @@ tomli==2.2.1 ; python_version == "3.10" \ --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 -twisted==26.4.0rc2 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:41eeb62a7f2688c634f7a1af76e225c58da48f8af1640fcb285ce386a96889e2 \ - --hash=sha256:d1db6c391a1b6e70028c3212298178621db149a6f75555f6503da557c763406d +twisted==24.11.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:695d0556d5ec579dcc464d2856b634880ed1319f45b10d19043f2b57eb0115b5 \ + --hash=sha256:fe403076c71f04d5d2d789a755b687c5637ec3bcd3b2b8252d76f2ba65f54261 txaio==23.1.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490 \ --hash=sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704 @@ -2660,9 +2626,9 @@ unicorn==2.1.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:b0f139adb1c9406f57d25cab96ad7a6d3cbb9119f5480ebecedd4f5d7cb024fb \ --hash=sha256:d4a08dbf222c5481bc909a9aa404b79874f6e67f5ba7c47036d03c68ab7371a7 \ --hash=sha256:f0ebcfaba67ef0ebcd05ee3560268f1c6f683bdd08ff496888741a163d29735d -urllib3==2.7.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +urllib3==2.3.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d uvicorn==0.18.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af \ --hash=sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b diff --git a/tests/test_analysis_manager.py b/tests/test_analysis_manager.py index e283558d2ed..b4ad3626e62 100644 --- a/tests/test_analysis_manager.py +++ b/tests/test_analysis_manager.py @@ -12,7 +12,7 @@ from lib.cuckoo.common.abstracts import Machinery from lib.cuckoo.common.config import Config, ConfigMeta from lib.cuckoo.core.analysis_manager import AnalysisManager -from lib.cuckoo.core.data.task import TASK_RUNNING, Task +from lib.cuckoo.core.data.task import TASK_RUNNING, Task, TASK_FAILED_ANALYSIS from lib.cuckoo.core.data.guests import Guest from lib.cuckoo.core.data.machines import Machine from lib.cuckoo.core.database import _Database @@ -440,3 +440,48 @@ class mock_sample: assert analysis_man.init_storage() is True mocker.patch("lib.cuckoo.core.database._Database.view_sample", return_value=mock_sample()) assert analysis_man.category_checks() is True + + def test_machine_running_finally_cleanup( + self, db: _Database, task: Task, machine: Machine, machinery_manager: MachineryManager, mocker: MockerFixture + ): + """Verify that machine_running stops and releases the machine even if an unhandled exception is raised in yield.""" + analysis_man = AnalysisManager(task=task, machine=machine, machinery_manager=machinery_manager) + + # Mock machinery manager functions + mock_start = mocker.patch.object(machinery_manager, "start_machine") + mock_stop = mocker.patch.object(machinery_manager, "stop_machine") + mock_release = mocker.patch.object(machinery_manager.machinery, "release") + + guest = mocker.MagicMock() + guest.id = 123 + analysis_man.guest = guest + + with pytest.raises(RuntimeError, match="Simulated unhandled exception"): + with analysis_man.machine_running(): + raise RuntimeError("Simulated unhandled exception") + + # Verify stop and release are still cleanly called on unhandled exceptions + assert mock_start.called + assert mock_stop.called + assert mock_release.called + + def test_launch_analysis_unexpected_exception( + self, db: _Database, task: Task, machine: Machine, machinery_manager: MachineryManager, mocker: MockerFixture + ): + """Verify that launch_analysis handles unexpected exceptions by setting status to failed and unlocking the machine.""" + analysis_man = AnalysisManager(task=task, machine=machine, machinery_manager=machinery_manager) + + # Force perform_analysis to raise an unhandled exception + mocker.patch.object(analysis_man, "perform_analysis", side_effect=RuntimeError("Unexpected perform_analysis error")) + mock_unlock = mocker.patch("lib.cuckoo.core.database._Database.unlock_machine") + mock_log_exception = mocker.patch.object(analysis_man.log, "exception") + + with pytest.raises(RuntimeError, match="Unexpected perform_analysis error"): + analysis_man.launch_analysis() + + # Verify task is flagged failed and machine is unlocked + with db.session.begin(): + db_task = db.view_task(task.id) + assert db_task.status == TASK_FAILED_ANALYSIS + assert mock_unlock.called + assert mock_log_exception.called diff --git a/uv.lock b/uv.lock index f8ffb47a98d..e43ca66a96d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.14' and platform_python_implementation == 'CPython'", diff --git a/web/apikey/context_processors.py b/web/apikey/context_processors.py index 9d389cbdf52..7298c618524 100644 --- a/web/apikey/context_processors.py +++ b/web/apikey/context_processors.py @@ -1,9 +1,9 @@ """Template context processor — surfaces apikey access policy to templates.""" -from .views import _user_may_manage_keys +from .policy import user_may_manage_keys def apikey_access(request): """Make `may_manage_apikeys` available to every template, so the user dropdown can hide the API Keys link for SSO non-staff users.""" - return {"may_manage_apikeys": _user_may_manage_keys(getattr(request, "user", None))} + return {"may_manage_apikeys": user_may_manage_keys(getattr(request, "user", None))} diff --git a/web/apikey/management/commands/okta_user_sync.py b/web/apikey/management/commands/okta_user_sync.py new file mode 100644 index 00000000000..5401dfcd3ed --- /dev/null +++ b/web/apikey/management/commands/okta_user_sync.py @@ -0,0 +1,132 @@ +"""Management command: reconcile local Django User.is_active with Okta status. + +For every Django User that has at least one active ApiKey AND a linked OIDC +SocialAccount, look the user up in Okta by email. If Okta no longer reports +the user as ACTIVE (status SUSPENDED, DEPROVISIONED, LOCKED_OUT, ...) or +returns no result for the email, deactivate the local user — which fires +the post_save cascade-revoke signal and invalidates all of their API keys. + +Run periodically via systemd timer (cape-okta-sync.timer) to bound the gap +between Okta-side disable/revoke and local API access being cut off. + +Configuration (web.conf [oauth_oidc]): + admin_api_url = https://.okta.com + admin_api_token = + +Usage: + poetry run python manage.py okta_user_sync # apply changes + poetry run python manage.py okta_user_sync --dry-run # report only +""" + +import logging + +import requests +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from allauth.socialaccount.models import SocialAccount +from apikey.models import ApiKey +from lib.cuckoo.common.config import Config + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Reconcile local User.is_active with Okta status; cascade-revoke ApiKeys for users no longer ACTIVE in Okta." + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="report what would change without modifying users", + ) + + def handle(self, *args, **opts): + web_cfg = Config("web") + oidc_cfg = getattr(web_cfg, "oauth_oidc", None) + if not oidc_cfg or not oidc_cfg.get("enabled", False): + self.stderr.write("[oauth_oidc] is not enabled — nothing to sync") + return + + admin_url = (oidc_cfg.get("admin_api_url") or "").rstrip("/") + admin_token = oidc_cfg.get("admin_api_token") or "" + if not admin_url or not admin_token: + self.stderr.write( + "[oauth_oidc] admin_api_url or admin_api_token missing — set them in web.conf to enable Okta sync" + ) + return + + # Find users that (a) have at least one active ApiKey and + # (b) have a linked SocialAccount (i.e., were JIT-provisioned via + # Okta SSO). Local-only admin accounts with API keys are skipped. + active_apikey_user_ids = set( + ApiKey.objects.filter(revoked_at__isnull=True).values_list("user_id", flat=True) + ) + sso_user_ids = set(SocialAccount.objects.values_list("user_id", flat=True)) + target_ids = active_apikey_user_ids & sso_user_ids + users = list(User.objects.filter(id__in=target_ids, is_active=True)) + if not users: + self.stdout.write("no SSO users with active API keys to check") + return + + self.stdout.write(f"checking {len(users)} SSO users with active ApiKeys against Okta") + + session = requests.Session() + session.headers.update( + { + "Authorization": f"SSWS {admin_token}", + "Accept": "application/json", + "User-Agent": "CAPE/okta_user_sync", + } + ) + + deactivated = 0 + for user in users: + email = (user.email or "").strip() + if not email: + self.stderr.write(f" user_id={user.id} ({user.username}): no email on local record — skipping") + continue + + try: + # search filter — exact email match. URL-quoting is handled by + # requests; escape backslashes and quotes so an address with + # those characters can't break the SCIM filter syntax. + safe_email = email.replace("\\", "\\\\").replace('"', '\\"') + r = session.get( + f"{admin_url}/api/v1/users", + params={"search": f'profile.email eq "{safe_email}"'}, + timeout=10, + ) + r.raise_for_status() + results = r.json() + except Exception as e: + self.stderr.write(f" {email}: Okta lookup failed: {e}") + continue + + if not isinstance(results, list) or not results: + # Email not found in Okta — user has been deleted there. + if self._deactivate(user, "okta_user_not_found", opts["dry_run"]): + deactivated += 1 + continue + + okta_user = results[0] + status = (okta_user.get("status") or "").upper() + if status != "ACTIVE": + if self._deactivate(user, f"okta_status_{status or 'UNKNOWN'}", opts["dry_run"]): + deactivated += 1 + else: + self.stdout.write(f" {email}: ACTIVE") + + suffix = " (dry run)" if opts["dry_run"] else "" + self.stdout.write(self.style.SUCCESS(f"sync complete{suffix} — {deactivated} user(s) deactivated")) + + def _deactivate(self, user, reason, dry_run): + msg = f" {user.email}: deactivating (reason={reason})" + if dry_run: + self.stdout.write(self.style.WARNING(f"{msg} [DRY RUN]")) + return False + user.is_active = False + user.save() # triggers the post_save cascade-revoke signal in apikey.signals + self.stdout.write(self.style.WARNING(msg)) + log.warning("okta_user_sync: deactivated user %s reason=%s", user.username, reason) + return True diff --git a/web/apikey/policy.py b/web/apikey/policy.py new file mode 100644 index 00000000000..00c6e7dae96 --- /dev/null +++ b/web/apikey/policy.py @@ -0,0 +1,19 @@ +"""API-key access policy — kept view-independent so both views.py and the +context processor can import it without pulling in view-layer dependencies +(or risking an import cycle).""" + +from allauth.socialaccount.models import SocialAccount + + +def user_may_manage_keys(user): + """Return True if `user` is allowed to view/create/revoke their own keys. + Local-only users always pass; SSO-provisioned users must be staff.""" + if not user or not user.is_authenticated: + return False + # Called from the apikey_access context processor on every page load — + # cache the SocialAccount lookup on the user object for the request to + # avoid a redundant query per render. + if not hasattr(user, "_may_manage_keys"): + is_sso = SocialAccount.objects.filter(user=user).exists() + user._may_manage_keys = True if not is_sso else bool(user.is_staff) + return user._may_manage_keys diff --git a/web/apikey/templates/apikey/list.html b/web/apikey/templates/apikey/list.html index 21f8d53ca3f..c4b15225630 100644 --- a/web/apikey/templates/apikey/list.html +++ b/web/apikey/templates/apikey/list.html @@ -55,7 +55,7 @@
Key created {% if not k.revoked_at %} -
+ {% csrf_token %}
diff --git a/web/apikey/views.py b/web/apikey/views.py index 9064f5045ca..8eaa3553a1b 100644 --- a/web/apikey/views.py +++ b/web/apikey/views.py @@ -15,7 +15,6 @@ their behalf, or admin creates a key for them in Django admin). """ -from allauth.socialaccount.models import SocialAccount from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect, render @@ -24,20 +23,7 @@ from .forms import ApiKeyCreateForm from .models import ApiKey - - -def _user_may_manage_keys(user): - """Return True if `user` is allowed to view/create/revoke their own keys. - Local-only users always pass; SSO-provisioned users must be staff.""" - if not user or not user.is_authenticated: - return False - # Called from the apikey_access context processor on every page load — - # cache the SocialAccount lookup on the user object for the request to - # avoid a redundant query per render. - if not hasattr(user, "_may_manage_keys"): - is_sso = SocialAccount.objects.filter(user=user).exists() - user._may_manage_keys = True if not is_sso else bool(user.is_staff) - return user._may_manage_keys +from .policy import user_may_manage_keys as _user_may_manage_keys def _forbidden(request): diff --git a/web/templates/account/account_inactive.html b/web/templates/account/account_inactive.html index 9612dcf6600..610bcb7e648 100644 --- a/web/templates/account/account_inactive.html +++ b/web/templates/account/account_inactive.html @@ -1,11 +1,18 @@ {% extends "base.html" %} - {% load i18n %} {% block head_title %}{% trans "Account Inactive" %}{% endblock %} {% block content %} -

{% trans "Account Inactive" %}

- -

{% trans "This account is inactive/Admin set manual approve." %}

+
+
+
+

{% trans "Account Inactive" %}

+
+
+

{% trans "This account is inactive. Contact an administrator if you believe this is in error." %}

+ {% trans "Back to login" %} +
+
+
{% endblock %} diff --git a/web/templates/account/login.html b/web/templates/account/login.html index 954e55d40c2..0e8a099b876 100644 --- a/web/templates/account/login.html +++ b/web/templates/account/login.html @@ -1,18 +1,58 @@ {% extends 'base.html' %} {% load i18n %} {% load crispy_forms_tags %} -{% block title %}Log In{% endblock title %} +{% load socialaccount %} +{% block title %}{% trans "Sign In" %}{% endblock title %} + {% block content %} -
-

{% trans "Sign In" %}

-

- {% blocktrans %}If you have not created an account yet, then please Sign up first. - {% endblocktrans %} -

-
- {% csrf_token %} - {{ form|crispy }} - {% trans "Forgot password?" %} -
+
+
+
+

{% trans "Sign In" %}

+
+
+ + {% if messages %} + {% for m in messages %} +
{{ m }}
+ {% endfor %} + {% endif %} + + {% get_providers as socialaccount_providers %} + {% if socialaccount_providers %} +
+ {% for provider in socialaccount_providers %} + + + {% blocktrans with name=provider.name %}Sign in with {{ name }}{% endblocktrans %} + + {% endfor %} +
+
+
+ {% trans "or sign in locally" %} +
+
+ {% endif %} + +
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+ +
+ +
+

+ {% blocktrans %}Don't have an account? Sign up.{% endblocktrans %} +

+
+
{% endblock content %} diff --git a/web/templates/account/logout.html b/web/templates/account/logout.html index 5dd48b0a6ea..eca1145da9b 100644 --- a/web/templates/account/logout.html +++ b/web/templates/account/logout.html @@ -1,14 +1,25 @@ {% extends 'base.html' %} +{% load i18n %} {% load crispy_forms_tags %} -{% block title %}Log Out{% endblock %} +{% block title %}{% trans "Sign Out" %}{% endblock %} + {% block content %} -
-

Log Out

-

Are you sure you want to log out?

-
- {% csrf_token %} - {{ form|crispy }} - -
+
+
+
+

{% trans "Sign Out" %}

+
+
+

{% trans "Are you sure you want to sign out?" %}

+
+ {% csrf_token %} + {{ form|crispy }} +
+ + {% trans "Cancel" %} +
+
+
+
{% endblock content %} diff --git a/web/templates/account/signup_closed.html b/web/templates/account/signup_closed.html index 9e8d44178b5..3ab65a4bc5c 100644 --- a/web/templates/account/signup_closed.html +++ b/web/templates/account/signup_closed.html @@ -1,11 +1,19 @@ {% extends "base.html" %} - {% load i18n %} {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} {% block content %} -

{% trans "Sign Up Closed" %}

- -

{% trans "We are sorry, but the sign up is currently closed." %}

+
+
+
+

{% trans "Sign Up Closed" %}

+
+
+

{% trans "We're sorry, but public sign-up is closed on this CAPE instance." %}

+

{% trans "If your organization uses single sign-on, return to the login page and click the SSO button — accounts are auto-provisioned for users your administrator has authorized." %}

+ {% trans "Back to login" %} +
+
+
{% endblock %} diff --git a/web/templates/socialaccount/authentication_error.html b/web/templates/socialaccount/authentication_error.html index bac67f17ad0..ad19eed4067 100644 --- a/web/templates/socialaccount/authentication_error.html +++ b/web/templates/socialaccount/authentication_error.html @@ -1,11 +1,25 @@ {% extends "base.html" %} - {% load i18n %} -{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} +{% block head_title %}{% trans "Authentication Error" %}{% endblock %} {% block content %} -

{% trans "Social Network Login Failure" %}

- -

{% trans "An error occurred while attempting to login via your social network account." %}

+
+
+
+

{% trans "Authentication Error" %}

+
+
+ {% if reason %} +

{{ reason }}

+ {% else %} +

{% trans "An error occurred while attempting to sign in." %}

+ {% endif %} + {% if auth_error.code %} +

{% trans "Code:" %} {{ auth_error.code }}

+ {% endif %} + {% trans "Try again" %} +
+
+
{% endblock %} diff --git a/web/templates/socialaccount/signup.html b/web/templates/socialaccount/signup.html index 64d33fa13ea..2853f36d351 100644 --- a/web/templates/socialaccount/signup.html +++ b/web/templates/socialaccount/signup.html @@ -1,22 +1,33 @@ {% extends "base.html" %} - {% load i18n %} +{% load crispy_forms_tags %} -{% block head_title %}{% trans "Signup" %}{% endblock %} +{% block head_title %}{% trans "Sign Up" %}{% endblock %} {% block content %} -

{% trans "Sign Up" %}

- -

{% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to -{{site_name}}. As a final step, please complete the following form:{% endblocktrans %}

- - +
+
+
+

{% trans "Complete your account" %}

+
+
+

+ {% blocktrans with provider_name=account.get_provider.name %}You're signing in with {{provider_name}} for the first time. Confirm a couple of details to finish creating your CAPE account:{% endblocktrans %} +

+ +
+
+
{% endblock %} diff --git a/web/web/allauth_adapters.py b/web/web/allauth_adapters.py index 03942011e9d..5d39e5b1b96 100644 --- a/web/web/allauth_adapters.py +++ b/web/web/allauth_adapters.py @@ -1,10 +1,267 @@ +import logging +import requests +import threading +import time + from allauth.account.adapter import DefaultAccountAdapter +from allauth.core.exceptions import ImmediateHttpResponse from allauth.socialaccount.adapter import DefaultSocialAccountAdapter -from allauth.account.signals import email_confirmed, user_signed_up +from allauth.account.signals import email_confirmed, user_logged_in, user_signed_up from django import forms from django.conf import settings from django.contrib.auth.models import User from django.dispatch import receiver +from django.shortcuts import render +from django.utils.translation import gettext as _ + + +log = logging.getLogger(__name__) + + +# ── OIDC IdP-response cache ────────────────────────────────────────────────── +# allauth fetches the OIDC discovery document and the JWKS on every login +# (cached only per-adapter, i.e. per-request) with no timeouts. A transient +# IdP hiccup or slow response then surfaces as a 500 to the user. +# +# This cache makes both fetches: +# • process-wide with a 1-hour TTL (5 minutes for JWKS so key rotation is +# picked up quickly) +# • bounded by 5 s connect / 10 s read timeouts +# • served from a stale entry on transient fetch errors instead of crashing +# • issuer-validated per RFC 8414 §3 (discovery only) +# • double-checked-locked so concurrent cold-start requests still see fresh data + +_OIDC_CACHE: dict = {} +_OIDC_CACHE_LOCK = threading.Lock() +_DISCOVERY_TTL = 3600 +_JWKS_TTL = 300 +# Asymmetric signing algorithms accepted for ID tokens when the provider's +# discovery doc doesn't advertise `id_token_signing_alg_values_supported`. +# Deliberately excludes "none" and the HMAC family to avoid algorithm-confusion. +_DEFAULT_OIDC_ALGS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"] + + +def _cached_fetch(cache_key: str, url: str, ttl: int, validate=None) -> dict: + """Fetch `url` as JSON with a process-wide TTL cache and stale-on-error. + + `validate(doc)` runs once on a freshly-fetched doc and may raise to reject + it; on rejection we fall back to the previously cached doc if any. + """ + now = time.monotonic() + with _OIDC_CACHE_LOCK: + entry = _OIDC_CACHE.get(cache_key) + if entry and (now - entry["ts"]) < ttl: + return entry["doc"] + + try: + resp = requests.get(url, timeout=(5, 10)) + resp.raise_for_status() + doc = resp.json() + if validate is not None: + validate(doc) + except (requests.RequestException, ValueError) as e: + # Re-read under the lock: another thread may have populated the cache + # while our fetch was in flight (concurrent cold start). Prefer any + # cached value — stale or freshly-won — over failing the login. + with _OIDC_CACHE_LOCK: + latest = _OIDC_CACHE.get(cache_key) + if latest is not None: + log.warning( + "OIDC fetch failed for %s (%s); serving cached value", + url, e, + ) + return latest["doc"] + log.error("OIDC fetch failed for %s with no cached fallback: %s", url, e) + raise + + with _OIDC_CACHE_LOCK: + existing = _OIDC_CACHE.get(cache_key) + if not existing or (time.monotonic() - existing["ts"]) >= ttl: + _OIDC_CACHE[cache_key] = {"doc": doc, "ts": time.monotonic()} + else: + doc = existing["doc"] + + return doc + + +def _get_cached_openid_config(server_url: str) -> dict: + def _validate_issuer(doc): + expected = server_url.replace("/.well-known/openid-configuration", "").rstrip("/") + actual = (doc.get("issuer") or "").rstrip("/") + if actual and actual != expected: + raise ValueError( + f"OIDC discovery issuer mismatch: expected {expected!r}, got {actual!r}" + ) + + return _cached_fetch( + cache_key=f"discovery:{server_url}", + url=server_url, + ttl=_DISCOVERY_TTL, + validate=_validate_issuer, + ) + + +def _get_cached_jwks(jwks_url: str) -> dict: + return _cached_fetch( + cache_key=f"jwks:{jwks_url}", + url=jwks_url, + ttl=_JWKS_TTL, + ) + + +try: + from allauth.socialaccount.providers.openid_connect.views import ( + OpenIDConnectOAuth2Adapter as _BaseOIDCAdapter, + ) + from allauth.socialaccount.providers.openid_connect.provider import ( + OpenIDConnectProvider as _BaseOIDCProvider, + ) + from allauth.socialaccount.internal import jwtkit + + class CachedOpenIDConnectOAuth2Adapter(_BaseOIDCAdapter): + """Serves the OIDC discovery doc and JWKS from a process-level cache, + with bounded timeouts and stale-on-error fallback. Without this an IdP + blip produces a 500 on the login flow.""" + + @property + def openid_config(self): + if not hasattr(self, "_openid_config"): + self._openid_config = _get_cached_openid_config( + self.get_provider().server_url + ) + return self._openid_config + + def _decode_id_token(self, app, id_token): + # Mirror allauth's default but route JWKS through our cache. + verify_signature = not self.did_fetch_access_token + keys_url = self.openid_config["jwks_uri"] + issuer = self.openid_config["issuer"] + if not verify_signature: + return jwtkit.verify_and_decode( + credential=id_token, + keys_url=keys_url, + issuer=issuer, + audience=app.client_id, + lookup_kid=jwtkit.lookup_kid_jwk, + verify_signature=verify_signature, + ) + + import jwt as _jwt + header = _jwt.get_unverified_header(id_token) + kid = header["kid"] + keys_data = _get_cached_jwks(keys_url) + key = jwtkit.lookup_kid_jwk(keys_data, kid) + if key is None: + # cache miss on a freshly-rotated kid — force a refresh + with _OIDC_CACHE_LOCK: + _OIDC_CACHE.pop(f"jwks:{keys_url}", None) + keys_data = _get_cached_jwks(keys_url) + key = jwtkit.lookup_kid_jwk(keys_data, kid) + if key is None: + from allauth.socialaccount.providers.oauth2.client import OAuth2Error + raise OAuth2Error(f"Invalid 'kid': '{kid}'") + # Pin accepted algorithms to the provider's advertised set rather + # than reflecting the token header's untrusted `alg`. PyJWT rejects + # a token whose alg isn't in this list, blocking "none"/HS* confusion. + allowed_algs = self.openid_config.get("id_token_signing_alg_values_supported") or _DEFAULT_OIDC_ALGS + data = _jwt.decode( + id_token, key=key, algorithms=allowed_algs, + issuer=issuer, audience=app.client_id, + leeway=30, + ) + jwtkit.verify_jti(data) + return data + + class CachedOpenIDConnectProvider(_BaseOIDCProvider): + """OpenID Connect provider using the cached adapter. + + Registered via SOCIALACCOUNT_PROVIDERS["openid_connect"]["provider_class"] + in settings.py — the officially-supported allauth override path. + """ + oauth2_adapter_class = CachedOpenIDConnectOAuth2Adapter + + @classmethod + def get_package(cls): + # allauth derives the URL module from get_package(); must point at + # the real openid_connect package so its urls.py is picked up by + # build_provider_urlpatterns(), not this module's package ("web"). + return "allauth.socialaccount.providers.openid_connect" + +except ImportError: + pass # openid_connect provider not installed — no-op + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def _extract_groups(extra: dict) -> set: + """Return the set of IdP group names from token extra data.""" + oidc_cfg = getattr(settings, "OIDC_CFG", None) or {} + claim = oidc_cfg.get("groups_claim") or "groups" + raw = extra.get(claim) or [] + if isinstance(raw, str): + raw = [raw] + elif not isinstance(raw, (list, tuple, set)): + # Unexpected claim shape (int/bool/dict/…) — don't crash the login. + log.warning("OIDC groups claim %r has unexpected type %s; ignoring", claim, type(raw).__name__) + return set() + return {g for g in raw if isinstance(g, str)} + + +def _group_set(config_key: str) -> set: + """Parse a comma-separated group list from OIDC_CFG into a set.""" + oidc_cfg = getattr(settings, "OIDC_CFG", None) or {} + return { + g.strip() + for g in (oidc_cfg.get(config_key) or "").split(",") + if g.strip() + } + + +def _apply_idp_roles_and_email(user, extra: dict) -> bool: + """Reconcile a user's email and is_staff/is_superuser against the IdP's + claims/groups, mutating `user` in place. Returns True if anything changed + (the caller is responsible for saving). + + Role mapping is applied only when admin_groups / superadmin_groups are + configured; otherwise roles are left untouched so they can be managed + manually in Django. When configured, membership is authoritative — a user + removed from the admin group in the IdP is demoted on their next login. + + Guard: if the groups claim is entirely *absent* from the token (scope or + claim-mapping misconfiguration, or a provider that drops it), role + reconciliation is skipped rather than silently demoting everyone. A present + but empty claim is honoured (the user really is in no groups → demote). + """ + changed = False + + email = extra.get("email") or "" + if email and user.email != email: + user.email = email + changed = True + + admin_groups = _group_set("admin_groups") + super_groups = _group_set("superadmin_groups") + if admin_groups or super_groups: + oidc_cfg = getattr(settings, "OIDC_CFG", None) or {} + claim = oidc_cfg.get("groups_claim") or "groups" + if claim not in extra: + log.warning( + "OIDC groups claim %r absent from token for %s; skipping role reconciliation", + claim, user.username, + ) + else: + user_groups = _extract_groups(extra) + new_staff = bool(user_groups & (admin_groups | super_groups)) + new_super = bool(user_groups & super_groups) + if user.is_staff != new_staff or user.is_superuser != new_super: + user.is_staff = new_staff + user.is_superuser = new_super + changed = True + + return changed + + +# ── Account adapters ────────────────────────────────────────────────────────── disposable_domain_list = [] if hasattr(settings, "DISPOSABLE_DOMAIN_LIST"): @@ -13,14 +270,11 @@ class DisposableEmails(DefaultAccountAdapter): - # https://fluffycloudsandlines.blog/using-django-allauth-for-google-login-to-any-django-app/ def clean_email(self, email): if email.rsplit("@", 1)[-1] in disposable_domain_list: raise forms.ValidationError("Admin banned disposable email services") - else: - return email + return email - # Enable/disable registration def is_open_for_signup(self, request): return settings.REGISTRATION_ENABLED @@ -39,23 +293,109 @@ def email_confirmed_(request, email_address, **kwargs): user.is_active = not settings.MANUAL_APPROVE user.save() + class MySocialAccountAdapter(DefaultSocialAccountAdapter): + def pre_social_login(self, request, sociallogin): - """ - Invoked just before a social login is about to proceed. - """ - user_email = sociallogin.account.extra_data.get("email") - if user_email and settings.SOCIAL_AUTH_EMAIL_DOMAIN: - domain = user_email.split("@")[1] + """Reject IdP accounts whose email domain doesn't match the configured + allowlist. Silently skipped when social_auth_email_domain is blank. + + Raises ImmediateHttpResponse — caught by allauth's complete_login + wrapper and rendered as a user-facing error page (a bare + ValidationError here would bubble up as a 500).""" + user_email = sociallogin.account.extra_data.get("email") or "" + if settings.SOCIAL_AUTH_EMAIL_DOMAIN: + if not user_email: + # Fail closed: a domain allowlist is configured but the IdP sent + # no email, so we can't verify the domain. Don't provision. + raise ImmediateHttpResponse( + render( + request, + "socialaccount/authentication_error.html", + {"reason": _("An email address is required to sign in.")}, + status=403, + ) + ) + domain = user_email.rsplit("@", 1)[-1] if domain != settings.SOCIAL_AUTH_EMAIL_DOMAIN: - raise forms.ValidationError(f"Please use email with domain: {settings.SOCIAL_AUTH_EMAIL_DOMAIN}") + raise ImmediateHttpResponse( + render( + request, + "socialaccount/authentication_error.html", + {"reason": _("Please use an email with domain: %(domain)s") + % {"domain": settings.SOCIAL_AUTH_EMAIL_DOMAIN}}, + status=403, + ) + ) - def save_user(self, request, sociallogin, form=None): + def is_open_for_signup(self, request, sociallogin): + """Gate account provisioning on IdP group membership. + + When required_groups is blank, any IdP-authenticated user gets a CAPE + account (appropriate when the Okta app assignment is the access gate). + When set, only users in at least one listed group are provisioned — + useful when the app is assigned broadly but CAPE access should be + restricted to a subset. """ - Saves a new User instance using information provided from social account provider. + required = _group_set("required_groups") + if not required: + return True + return bool(_extract_groups(sociallogin.account.extra_data or {}) & required) + + def save_user(self, request, sociallogin, form=None): + """Provision a new SSO user: derive a stable username and apply the + initial email + IdP-group roles. + + This runs once, when the social identity is first linked. Email and + role reconciliation on *subsequent* logins is handled by the + ``_reconcile_sso_user_on_login`` receiver below — allauth does not call + save_user for returning users — so changes to a user's IdP group + membership (e.g. removal from an admin group) take effect on their next + sign-in. + + Username: derived from the email local-part. If that collides with an + existing account (two identities sharing a local-part across different + domains), a short suffix from the IdP subject claim — or the user's pk + if no subject is present — is appended to guarantee uniqueness. """ - user = super(MySocialAccountAdapter, self).save_user(request, sociallogin, form) - user.email = sociallogin.account.extra_data.get("email") - user.username = sociallogin.account.extra_data.get("email").split("@")[0] + user = super().save_user(request, sociallogin, form) + extra = sociallogin.account.extra_data or {} + + # ── username (provisioning only — kept stable across later logins) ── + identifier = ( + extra.get("email") + or extra.get("preferred_username") + or extra.get("sub") + or user.username + or "" + ) + if identifier: + base = (identifier.split("@")[0] if "@" in identifier else identifier)[:150] + if User.objects.filter(username=base).exclude(pk=user.pk).exists(): + # Reserve room for the suffix so truncation can't drop the bit + # that makes the name unique (and can't exceed the 150 limit). + suffix = "_" + ((extra.get("sub") or "")[:8] or str(user.pk)) + base = base[: 150 - len(suffix)] + suffix + user.username = base + + _apply_idp_roles_and_email(user, extra) user.save() return user + + +@receiver(user_logged_in) +def _reconcile_sso_user_on_login(sender, request, user, **kwargs): + """Reconcile email + IdP-group roles on every SSO login. + + allauth fires ``user_logged_in`` after a successful login and, for social + logins, passes the ``sociallogin``. Because ``save_user`` only runs at first + provisioning, this receiver is what makes role demotion/promotion and email + changes actually propagate when a returning user's IdP attributes change. + Local (non-SSO) logins carry no ``sociallogin`` and are left untouched. + """ + sociallogin = kwargs.get("sociallogin") + if sociallogin is None: + return + extra = getattr(sociallogin.account, "extra_data", None) or {} + if _apply_idp_roles_and_email(user, extra): + user.save() diff --git a/web/web/local_settings.py b/web/web/local_settings.py index 566f5b4e88f..41c6fd2c0ef 100644 --- a/web/web/local_settings.py +++ b/web/web/local_settings.py @@ -38,20 +38,14 @@ # STATIC_ROOT = "" # STATIC_ROOT = os.path.join(os.getcwd(), "static") -SOCIALACCOUNT_PROVIDERS = { - "google": { - "SCOPE": [ - "profile", - "email", - ], - "AUTH_PARAMS": { - "access_type": "online", - }, - }, - "github": { - "SCOPE": [ - "read:user", - "user:email", - ], - }, -} +# SOCIALACCOUNT_PROVIDERS removed: managed dynamically from web.conf [oauth_oidc] in settings.py. +# The previous stub here (google + github) was dead — the provider apps were commented out in INSTALLED_APPS. + +# Session lifetime: 8-hour sliding idle timeout. SESSION_SAVE_EVERY_REQUEST +# resets the SESSION_COOKIE_AGE window on each request, so the session expires +# only after 8h of *inactivity* (not 8h absolute). Combined with django-allauth +# + Okta SSO, an idle user is forced back through Okta, capping the gap between +# an Okta account disable/lockout and CAPE access being revoked. Note: saving +# the session every request adds session-store writes; fine for this scale. +SESSION_COOKIE_AGE = 28800 +SESSION_SAVE_EVERY_REQUEST = True diff --git a/web/web/settings.py b/web/web/settings.py index 2f314897a0d..65d9135041c 100644 --- a/web/web/settings.py +++ b/web/web/settings.py @@ -17,6 +17,7 @@ RUNNING_TESTS = "test" in sys.argv +from django.core.exceptions import ImproperlyConfigured from lib.cuckoo.common.config import Config # In case we have VPNs enabled we need to initialize through the following @@ -276,21 +277,64 @@ "rest_framework.authtoken", # Per-user labeled API keys (multi-key, individually revocable). Lives # alongside DRF's legacy `authtoken` so ApiKeyAuthentication can fall - # back to existing tokens for back-compat. - "apikey", + # back to existing tokens for back-compat. Reference the AppConfig + # explicitly so its ready() (disable-cascade signal wiring) always loads. + "apikey.apps.ApiKeyConfig", ] +# OpenID Connect (Okta / Azure AD / Auth0 / Google Workspace / Keycloak / +# any OIDC-compliant IdP) is wired through django-allauth's generic +# `openid_connect` provider — registered conditionally so the dependency +# stays inert when SSO is disabled. Configure via [oauth_oidc] in web.conf. +OIDC_CFG = getattr(web_cfg, "oauth_oidc", None) +if OIDC_CFG is not None and OIDC_CFG.get("enabled", False): + _missing = [k for k in ("client_id", "client_secret", "server_url") if not (OIDC_CFG.get(k) or "").strip()] + if _missing: + raise ImproperlyConfigured( + f"[oauth_oidc] enabled = yes but required fields are blank: {', '.join(_missing)}. Check conf/web.conf." + ) + INSTALLED_APPS.append("allauth.socialaccount.providers.openid_connect") + # Merge into any existing provider config instead of reassigning, so + # enabling OIDC doesn't clobber other providers a deployment may have set. + SOCIALACCOUNT_PROVIDERS = globals().get("SOCIALACCOUNT_PROVIDERS") or {} + SOCIALACCOUNT_PROVIDERS["openid_connect"] = { + # Use our subclass for process-level discovery-doc / JWKS caching. + "provider_class": "web.allauth_adapters.CachedOpenIDConnectProvider", + "APPS": [ + { + "provider_id": OIDC_CFG.get("provider_id", "oidc"), + "name": OIDC_CFG.get("name", "OIDC"), + "client_id": OIDC_CFG.get("client_id", ""), + "secret": OIDC_CFG.get("client_secret", ""), + "settings": { + "server_url": OIDC_CFG.get("server_url", ""), + }, + } + ], + } + AUDIT_FRAMEWORK = web_cfg.audit_framework.get("enabled", False) if api_cfg.api.token_auth_enabled: + # Per-user labeled API keys; ApiKeyAuthentication internally falls back to + # DRF's legacy TokenAuthentication so tokens issued via + # /apiv2/api-token-auth/ keep working without migration. + # + # When SSO is enabled, scripts targeting /apiv2/ MUST present an explicit + # API key (Authorization: Token ) — we drop SessionAuthentication from + # the default chain so a browser session cookie issued via the IdP can't + # authenticate API calls. Browser-only flows (apikey list/create pages, etc.) + # live outside DRF and continue to use Django session auth. Specific + # UI-internal endpoints that legitimately need cookie auth can opt back in + # per-view with @authentication_classes([SessionAuthentication]). + _api_auth_classes = ["apikey.authentication.ApiKeyAuthentication"] + if not (OIDC_CFG and OIDC_CFG.get("enabled", False)): + # No SSO configured — keep the legacy session-cookie fallback for + # back-compat with deployments that script against /apiv2/ using + # their browser cookies. + _api_auth_classes.append("rest_framework.authentication.SessionAuthentication") REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": [ - # Per-user labeled API keys; internally falls back to DRF's legacy - # TokenAuthentication so tokens issued via /apiv2/api-token-auth/ - # keep working without migration. - "apikey.authentication.ApiKeyAuthentication", - "rest_framework.authentication.SessionAuthentication", - ], + "DEFAULT_AUTHENTICATION_CLASSES": _api_auth_classes, "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_THROTTLE_CLASSES": ["apiv2.throttling.SubscriptionRateThrottle"], "DEFAULT_THROTTLE_RATES": { @@ -368,13 +412,18 @@ EMAIL_CONFIRMATION = web_cfg.registration.get("email_confirmation", False) SOCIAL_AUTH_EMAIL_DOMAIN = web_cfg.web_auth.get("social_auth_email_domain", False) -# be careful with SOCIALACCOUNT_AUTO_SIGNUP, if True, it will bypass custom sighup functions, default is True -# SOCIALACCOUNT_AUTO_SIGNUP = True +# Activate the social adapter so OIDC signups bypass the REGISTRATION_ENABLED +# toggle (the public-signup form gate). Users coming through the IdP have +# already been vetted and explicitly assigned to the app, so they shouldn't be +# blocked by the same flag that closes anonymous signup. The adapter also +# enforces the email-domain allowlist and IdP-group role mapping. +SOCIALACCOUNT_ADAPTER = "web.allauth_adapters.MySocialAccountAdapter" +SOCIALACCOUNT_AUTO_SIGNUP = True +# Send the user straight to the IdP when they click the SSO button, skipping +# allauth's intermediate confirmation page. (Login is initiated via GET.) +SOCIALACCOUNT_LOGIN_ON_GET = True # SOCIALACCOUNT_ONLY = True -# SOCIALACCOUNT_LOGIN_ON_GET=True # ACCOUNT_SIGNUP_FORM_CLASS = None -# In case you want to verify domain of email + set the username -# SOCIALACCOUNT_ADAPTER = 'web.allauth_adapters.MySocialAccountAdapter' # ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" #### AllAuth end @@ -421,6 +470,19 @@ ALLOWED_HOSTS = ["*"] +# Reverse-proxy TLS termination: when nginx terminates HTTPS and forwards plain +# HTTP to gunicorn/daphne, Django must be told via X-Forwarded-Proto that the +# original request was HTTPS — otherwise request.is_secure() is False and the +# absolute OIDC redirect_uri django-allauth builds comes out as http://…, which +# the IdP rejects. Requires `proxy_set_header X-Forwarded-Proto $scheme;` in nginx. +# +# Gated behind [general] behind_proxy because trusting X-Forwarded-Proto/Host is +# only safe when a reverse proxy strips/overwrites those headers from clients — +# enabling them with a directly-reachable app would allow proto/host spoofing. +if web_cfg.general.get("behind_proxy", False): + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + USE_X_FORWARDED_HOST = True + # Max size MAX_UPLOAD_SIZE = web_cfg.general.max_sample_size # Google's OAuth might need: "strict-origin-when-cross-origin"