Merge remote-tracking branch 'origin/master' into mintlify/cb114598

This commit is contained in:
mintlify[bot] authored and GitHub committed 2026-10-12 07:07:23 +00:00
commit f7660cf1b4
76 files changed
+2341 -608

No files matched your search

+7 -7
View File
@@ -19,10 +19,10 @@
# AI
/mcp_server/ @prowler-cloud/engineering
# Platform
/.github/ @prowler-cloud/platform
/Makefile @prowler-cloud/platform
/kubernetes/ @prowler-cloud/platform
**/Dockerfile* @prowler-cloud/platform
**/docker-compose*.yml @prowler-cloud/platform
**/docker-compose*.yaml @prowler-cloud/platform
# CI and infrastructure
/.github/ @prowler-cloud/engineering
/Makefile @prowler-cloud/engineering
/kubernetes/ @prowler-cloud/engineering
**/Dockerfile* @prowler-cloud/engineering
**/docker-compose*.yml @prowler-cloud/engineering
**/docker-compose*.yaml @prowler-cloud/engineering
+27 -2
View File
@@ -84,6 +84,8 @@ jobs:
fi
- name: Checkout release branch
env:
GH_TOKEN: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
run: |
echo "Checking out branch $BRANCH_NAME for release $PROWLER_VERSION..."
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
@@ -92,8 +94,32 @@ jobs:
elif git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
echo "Branch $BRANCH_NAME exists remotely, checking out..."
git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME"
elif [ "$PATCH_VERSION" -eq 0 ]; then
# Only cut the branch once the compiled changelogs are on master
MASTER_SHA=$(git rev-parse origin/master)
VERSION_REGEX="${PROWLER_VERSION//./\\.}"
FOUND_CHANGELOG=false
for changelog in prowler/CHANGELOG.md api/CHANGELOG.md ui/CHANGELOG.md mcp_server/CHANGELOG.md; do
if git show "origin/master:$changelog" 2>/dev/null | grep -qE "^## \[.*\(Prowler v?${VERSION_REGEX}\)"; then
FOUND_CHANGELOG=true
break
fi
done
if [ "$FOUND_CHANGELOG" != "true" ]; then
echo "ERROR: No changelog entry for Prowler $PROWLER_VERSION on master. Merge the compiled changelogs PR first."
exit 1
fi
# Check the SDK version on master before creating the remote branch
if ! git show "origin/master:pyproject.toml" | grep -qE "^version = \"${VERSION_REGEX}\"$" \
|| ! git show "origin/master:prowler/config/config.py" | grep -qE "^prowler_version = \"${VERSION_REGEX}\"$"; then
echo "ERROR: pyproject.toml or prowler/config/config.py on master is not at version $PROWLER_VERSION. Fix master before creating $BRANCH_NAME."
exit 1
fi
echo "Branch $BRANCH_NAME does not exist, creating it from master ($MASTER_SHA)..."
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/heads/${BRANCH_NAME}" -f sha="${MASTER_SHA}"
git checkout -b "$BRANCH_NAME" "$MASTER_SHA"
else
echo "ERROR: Branch $BRANCH_NAME does not exist. For minor releases (X.Y.0), create it manually first. For patch releases (X.Y.Z), the branch should already exist."
echo "ERROR: Branch $BRANCH_NAME does not exist. For patch releases (X.Y.Z), the branch should already exist."
exit 1
fi
@@ -390,4 +416,3 @@ jobs:
if: always()
run: |
rm -f prowler_changelog.md api_changelog.md ui_changelog.md mcp_changelog.md combined_changelog.md
+27
View File
@@ -0,0 +1,27 @@
name: 'Tools: Release Freeze Lift'
run-name: 'Lift release freeze after Prowler ${{ github.event.release.tag_name }}'
on:
release:
types:
- 'published'
permissions: {}
jobs:
release-freeze-lift:
if: github.repository == 'prowler-cloud/prowler' && !github.event.release.prerelease
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Disable release freeze
env:
GH_TOKEN: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
run: |
gh variable set RELEASE_FREEZE --body false --repo "${GITHUB_REPOSITORY}"
+15
View File
@@ -126,3 +126,18 @@ ignore:
- vulnerability: CVE-2026-82049
package:
name: python
# zlib in the Alpine base images of the UI (node:24.18.1-alpine) and MCP
# (python:3.13.14-alpine3.23) containers. CVE-2026-85091 is a heap overflow in
# gz_vacate() reached only through non-blocking gzwrite() followed by gzprintf() after
# a write stall. Node links its own bundled zlib and the MCP server never writes gzip
# files, so neither image calls that path. TEMPORARY: the 1.3.2-r1 fix is in the
# Alpine index and could be taken with `apk add --upgrade`, but we wait for the base
# images to ship it instead (the newest node:24-alpine and python:3.13-alpine3.23 still
# carry 1.3.2-r0). Pinned to that version so the rule stops matching on its own once a
# base image moves forward. Remove when the base digests are bumped, by 2026-11-07.
# https://security.alpinelinux.org/vuln/CVE-2026-85091
- vulnerability: CVE-2026-85091
package:
name: zlib
version: 1.3.2-r0
+62 -53
View File
@@ -73,6 +73,13 @@ vulnerabilities:
# there is no Node runtime and no node_modules anywhere in the image, and the .NET
# assemblies target net472, a Windows-only framework. Nothing here is reachable, and none
# of it is a dependency we declare -- only Microsoft can change the module's contents.
# proxy-addr 2.0.7 (CVE-2026-90711) is one more such declaration: no file named proxy-addr
# exists in the image, only its purl inside that manifest. The UI lock lists proxy-addr 2.0.7
# through express, but the Next standalone output does not ship it, so the shared entry
# hides nothing real in the UI image either (checked on prowlercloud/prowler-ui:latest).
# @modelcontextprotocol/sdk 1.27.1 (CVE-2026-104850) is another: only its purl is in that
# manifest. The entry is pinned to 1.27.1 because the UI really depends on the SDK (1.26.0
# via @langchain/mcp-adapters), and a version-less purl would hide that finding too.
- id: CVE-2020-0606
purls:
- "pkg:nuget/Microsoft.WindowsDesktop.App.Ref"
@@ -137,62 +144,64 @@ vulnerabilities:
purls:
- "pkg:npm/ip-address"
expired_at: 2027-01-31
- id: CVE-2026-101898
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101901
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101903
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101905
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101906
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101907
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-101909
purls:
- "pkg:npm/axios"
expired_at: 2027-01-31
- id: CVE-2026-90711
purls:
- "pkg:npm/proxy-addr"
expired_at: 2027-01-31
- id: CVE-2026-104850
purls:
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
# CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into
# millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in
# 1.83.1 (published 2026-09-01). Trivy 0.74.0, the latest published release and the
# version the images ship, pins 1.82.1 as an indirect dependency:
# https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod
# Upstream bump still open: https://github.com/aquasecurity/trivy/pull/11176
# Trivy only speaks gRPC in client/server mode (`trivy server`, `--server`). Prowler
# invokes it exclusively as `trivy image` and `trivy fs` on a local path, so no gRPC
# listener or connection ever exists in the image and the affected path is not
# reachable. Remove this temporary suppression as soon as a Trivy release pins
# grpc >= 1.83.1.
- id: CVE-2026-84304
# CVE-2026-78667 and CVE-2026-97031 are DoS bugs in the Go standard library: net/http
# burns CPU parsing a Range header with a large number of small ranges in FileServer,
# ServeContent and ServeFile, and crypto/tls lets a client exhaust server memory with
# multiple ECH outer extension references. Both are server-side code paths. Fixed in
# Go 1.26.9 and 1.27.2 (released 2026-10-08). Trivy 0.75.0, the latest published
# release and the version the images ship, is built with go1.27.1, and Trivy main
# still declares `go 1.27.0`:
# https://github.com/aquasecurity/trivy/blob/v0.75.0/go.mod
# Prowler invokes Trivy exclusively as `trivy fs` and `trivy image` on a local path,
# never `trivy server`, so no HTTP or TLS listener ever exists in the image; its only
# TLS use is as a client pulling the vulnerability database. The purl is pinned to the
# stdlib version so the entry retires itself on the next Trivy bump. Remove this
# temporary suppression as soon as a Trivy release is built with Go >= 1.27.2.
- id: CVE-2026-78667
purls:
- "pkg:golang/google.golang.org/grpc"
expired_at: 2026-10-15
# CVE-2026-84445 is a DoS in grpc-go servers built with `xds.NewGRPCServer()`: a request
# carrying neither `:authority` nor `Host` reaches the xDS routing interceptor, which
# indexes an empty slice of authorities and panics. The per-RPC goroutine does not
# recover, so the whole server process dies. Fixed in 1.82.2 and 1.83.2 (published
# 2026-09-08). Trivy 0.74.0, the latest published release and the version the images
# ship, pins 1.82.1 as an indirect dependency:
# https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod
# Trivy main already carries 1.83.2, but no published release includes it yet.
# The reachability argument is the one made for CVE-2026-84304 above, only narrower:
# this panic needs an xDS-managed gRPC server. Prowler invokes Trivy exclusively as
# `trivy image` and `trivy fs` on a local path, never `trivy server`, so the image runs
# no gRPC server at all, xDS or otherwise. Remove this temporary suppression as soon as
# a Trivy release pins grpc >= 1.83.2.
# https://github.com/advisories/GHSA-2v4p-qf9q-27wj
- id: CVE-2026-84445
- "pkg:golang/stdlib@v1.27.1"
expired_at: 2026-11-15
- id: CVE-2026-97031
purls:
- "pkg:golang/google.golang.org/grpc@v1.82.1"
expired_at: 2026-10-15
# CVE-2026-56855 and CVE-2026-78662 are DoS deadlocks in x/crypto/ssh: a malicious peer
# can flood or misuse channel messages (RFC 4254) to block the whole connection.
# Fixed in golang.org/x/crypto v0.56.0 (published 2026-09-02). Trivy 0.74.0, the latest
# published release and the version the images ship, still pins v0.55.0, and Trivy main
# has not bumped it either:
# https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod
# x/crypto/ssh is pulled in transitively through go-git's ssh transport, the same
# dependency chain as the CVE-2026-71556 entry above. Prowler invokes Trivy only with
# `fs` on an existing local path or with `image`; it never asks Trivy to clone over SSH
# or to run `trivy server`, so no SSH connection -- as client or server -- ever exists in
# the image and the affected code path is not reachable. Remove this temporary
# suppression as soon as a fixed Trivy release is available.
- id: CVE-2026-56855
purls:
- "pkg:golang/golang.org/x/crypto@v0.55.0"
expired_at: 2026-10-15
- id: CVE-2026-78662
purls:
- "pkg:golang/golang.org/x/crypto@v0.55.0"
expired_at: 2026-10-15
- "pkg:golang/stdlib@v1.27.1"
expired_at: 2026-11-15
- id: CVE-2026-56852
purls:
+4 -4
View File
@@ -1,4 +1,4 @@
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
@@ -8,15 +8,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.74.0
ARG TRIVY_VERSION=0.75.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
+4 -4
View File
@@ -1,4 +1,4 @@
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
LABEL maintainer="https://github.com/prowler-cloud/api"
@@ -7,15 +7,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.74.0
ARG TRIVY_VERSION=0.75.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
@@ -0,0 +1 @@
Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the API container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy
@@ -0,0 +1 @@
`django` from 5.1.15 to 5.2.17, patching CVE-2026-15307
@@ -0,0 +1 @@
`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx
@@ -0,0 +1 @@
Kubernetes kubeconfig validation accepts only inline credentials and known cluster fields, rejecting file-path fields (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), command-based authentication (`exec`, `cmd-path`), `auth-provider` directives, proxy URLs and unknown user or cluster keys; stored kubeconfigs are re-validated before each scan and connection test
@@ -0,0 +1 @@
`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749
@@ -0,0 +1 @@
`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689
@@ -0,0 +1 @@
`GET /api/v1/users/{id}` no longer exposes another user's memberships, roles or tenant names from tenants other than the one in the request token
+12 -10
View File
@@ -25,7 +25,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
dependencies = [
"celery (==5.6.2)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
"django (==5.1.15)",
"django (==5.2.17)",
"django-allauth[saml] (==65.15.0)",
"django-celery-beat (==2.9.0)",
"django-celery-results (==2.6.0)",
@@ -222,7 +222,7 @@ constraint-dependencies = [
"dill==0.4.1",
"distro==1.9.0",
"dj-rest-auth==7.0.1",
"django==5.1.15",
"django==5.2.17",
"django-allauth==65.15.0",
"django-celery-beat==2.9.0",
"django-celery-results==2.6.0",
@@ -246,7 +246,7 @@ constraint-dependencies = [
"drf-simple-apikey==2.2.1",
"drf-spectacular==0.27.2",
"drf-spectacular-jsonapi==0.5.1",
"dulwich==1.2.5",
"dulwich==1.2.9",
"duo-client==5.5.0",
"durationpy==0.10",
"email-validator==2.2.0",
@@ -439,7 +439,7 @@ constraint-dependencies = [
"tzdata==2025.3",
"tzlocal==5.3.1",
"uritemplate==4.2.0",
"urllib3==2.7.0",
"urllib3==2.8.0",
"uuid6==2024.7.10",
"uvicorn==0.49.0",
"uvloop==0.22.1",
@@ -475,15 +475,17 @@ constraint-dependencies = [
# a constraint cannot satisfy against the SDK's hard pins, so override the whole set
# to 1.9.10 until the SDK bump propagates to the pinned master rev.
#
# prowler@master hard-pins dulwich==0.23.0 and pyjwt==2.12.1 in [project.dependencies].
# dulwich 1.2.5 patches GHSA-897w-fcg9-f6xj (arbitrary file write) and pyjwt 2.14.0
# patches GHSA-ffc3-869f-jxw9 (HMAC/PEM key-confusion); a constraint cannot satisfy these
# against the SDK's hard pins, so override them to the patched versions until the SDK
# bump propagates to the pinned master rev. pyjwt keeps the [crypto] extra because an
# The API installs prowler from a pinned master git rev whose dulwich and pyjwt pins trail
# the root pyproject.toml. dulwich 1.2.9 patches GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf,
# GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx, and pyjwt 2.14.0 patches GHSA-ffc3-869f-jxw9,
# so override both until that rev is refreshed. pyjwt keeps the [crypto] extra because an
# override replaces the whole requirement; bare pyjwt would drop it from the consumers
# that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive.
override-dependencies = [
"okta==3.4.2",
# huaweicloudsdkcore (via prowler) pins pymongo==4.15.1 exactly; CVE-2026-96748 and
# CVE-2026-96749 are fixed in 4.18.2. Only bson types are used, so the override is safe.
"pymongo==4.18.2",
# prowler requires cryptography==50.0.0. Two api-only dependencies still cap it below
# 49 and cannot move yet: msal, pinned exactly by azure-cli-core (2.83.0 -> 1.35.0b1,
# 2.89.1 -> 1.36.0, both <49; cartography needs azure-cli-core), and workos 8.3.0
@@ -499,6 +501,6 @@ override-dependencies = [
"microsoft-kiota-serialization-json==1.9.10",
"microsoft-kiota-serialization-multipart==1.9.10",
"microsoft-kiota-serialization-text==1.9.10",
"dulwich==1.2.5",
"dulwich==1.2.9",
"pyjwt[crypto]==2.14.0"
]
@@ -5,6 +5,8 @@ from api.v1.serializer_utils.integrations import (
)
from api.v1.serializer_utils.providers import ProviderSecretField
from api.v1.serializers import (
KUBERNETES_KUBECONFIG_INVALID_ERROR,
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
ImageProviderSecret,
IntegrationSerializer,
IntegrationUpdateSerializer,
@@ -340,6 +342,278 @@ current-context: test-context
assert not serializer.is_valid()
assert "kubeconfig_content" in serializer.errors
@staticmethod
def _kubeconfig(user_extra="", cluster_extra=""):
return f"""
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://kubernetes.example.test
{cluster_extra}
users:
- name: test-user
user:
token: test-token
{user_extra}
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
"""
@staticmethod
def _assert_rejected_without_echo(
kubeconfig_content, leaked_value, expected_message
):
serializer = KubernetesProviderSecret(
data={"kubeconfig_content": kubeconfig_content}
)
assert serializer.is_valid() is False
assert serializer.errors["kubeconfig_content"] == [expected_message]
assert leaked_value not in str(serializer.errors)
def test_inline_token_with_certificate_authority_data_is_accepted(self):
kubeconfig_content = self._kubeconfig(
cluster_extra=" certificate-authority-data: dGVzdA=="
)
serializer = KubernetesProviderSecret(
data={"kubeconfig_content": kubeconfig_content}
)
assert serializer.is_valid()
def test_kubeconfig_with_token_file_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(user_extra=" tokenFile: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_client_certificate_file_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(user_extra=" client-certificate: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_client_key_file_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(user_extra=" client-key: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_certificate_authority_file_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(cluster_extra=" certificate-authority: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_cluster_proxy_url_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(cluster_extra=" proxy-url: http://proxy.evil.test"),
"proxy.evil.test",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
@pytest.mark.parametrize("provider_name", ["gcp", "oidc", "azure"])
def test_kubeconfig_with_any_auth_provider_is_rejected(self, provider_name):
self._assert_rejected_without_echo(
self._kubeconfig(
user_extra=(
" auth-provider:\n"
f" name: {provider_name}\n"
" config:\n"
" idp-issuer-url: https://idp.evil.test"
)
),
"idp.evil.test",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_unknown_user_key_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(user_extra=" bogus: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_unknown_cluster_key_is_rejected(self):
self._assert_rejected_without_echo(
self._kubeconfig(cluster_extra=" bogus: /etc/passwd"),
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_allowlisted_cluster_keys_is_accepted(self):
serializer = KubernetesProviderSecret(
data={
"kubeconfig_content": self._kubeconfig(
cluster_extra=(
" certificate-authority-data: dGVzdA==\n"
" insecure-skip-tls-verify: false\n"
" tls-server-name: kubernetes.example.test\n"
" disable-compression: true\n"
" extensions: []"
)
)
}
)
assert serializer.is_valid(), serializer.errors
def test_kubeconfig_with_non_list_clusters_is_rejected(self):
kubeconfig_content = """
apiVersion: v1
kind: Config
clusters:
name: /etc/passwd
"""
self._assert_rejected_without_echo(
kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR
)
def test_kubeconfig_with_non_dict_cluster_entry_is_rejected(self):
kubeconfig_content = """
apiVersion: v1
kind: Config
clusters:
- /etc/passwd
"""
self._assert_rejected_without_echo(
kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR
)
def test_kubeconfig_with_non_dict_cluster_value_is_rejected(self):
kubeconfig_content = """
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster: /etc/passwd
"""
self._assert_rejected_without_echo(
kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR
)
@pytest.mark.parametrize(
"user_extra",
[
" token: test-token",
" username: test-user\n password: test-password",
" client-certificate-data: dGVzdA==\n client-key-data: dGVzdA==",
" token: test-token\n as: other-user",
" token: test-token\n as-uid: 1234",
" token: test-token\n as-groups:\n - group-a",
" token: test-token\n as-user-extra:\n scopes:\n - read",
],
ids=[
"token",
"username-password",
"client-cert-data",
"as",
"as-uid",
"as-groups",
"as-user-extra",
],
)
def test_kubeconfig_with_allowlisted_user_keys_is_accepted(self, user_extra):
kubeconfig_content = f"""
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://kubernetes.example.test
certificate-authority-data: dGVzdA==
users:
- name: test-user
user:
{user_extra}
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
"""
serializer = KubernetesProviderSecret(
data={"kubeconfig_content": kubeconfig_content}
)
assert serializer.is_valid()
@pytest.mark.parametrize(
"kubeconfig_content",
[
"""
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://kubernetes.example.test
users:
- name: good-user
user:
token: test-token
- name: bad-user
user:
tokenFile: /etc/passwd
""",
"""
apiVersion: v1
kind: Config
clusters:
- name: good-cluster
cluster:
server: https://kubernetes.example.test
- name: bad-cluster
cluster:
server: https://kubernetes.example.test
certificate-authority: /etc/passwd
users:
- name: test-user
user:
token: test-token
""",
],
ids=["second-user", "second-cluster"],
)
def test_kubeconfig_with_bad_second_entry_is_rejected(self, kubeconfig_content):
self._assert_rejected_without_echo(
kubeconfig_content,
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_kubeconfig_with_indented_document_is_rejected(self):
kubeconfig_content = (
" wrapper:\n"
" users:\n"
" - name: u\n"
" user:\n"
" tokenFile: /etc/passwd\n"
" clusters:\n"
" - name: c\n"
" cluster:\n"
" server: https://example.test\n"
)
self._assert_rejected_without_echo(
kubeconfig_content,
"/etc/passwd",
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
)
def test_malformed_kubeconfig_is_rejected(self):
serializer = KubernetesProviderSecret(
data={"kubeconfig_content": "apiVersion: ["}
+96 -4
View File
@@ -14,6 +14,7 @@ from api.utils import (
return_prowler_provider,
validate_invitation,
)
from api.v1.serializers import KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
from prowler.providers.alibabacloud.alibabacloud_provider import AlibabacloudProvider
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
@@ -35,6 +36,34 @@ from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvid
from prowler.providers.vercel.vercel_provider import VercelProvider
from rest_framework.exceptions import NotFound, ValidationError
VALID_KUBECONFIG = """\
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.invalid:6443
certificate-authority-data: Y2E=
users:
- name: test-user
user:
token: test-token
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
"""
TOKEN_FILE_KUBECONFIG = VALID_KUBECONFIG.replace(
"token: test-token", "tokenFile: /etc/passwd"
)
EXEC_KUBECONFIG = VALID_KUBECONFIG.replace(
"token: test-token", "exec:\n command: test-command"
)
class TestMergeDicts:
def test_simple_merge(self):
@@ -288,6 +317,40 @@ class TestProwlerProviderConnectionTest:
raise_on_exception=False,
)
@patch("api.utils.return_prowler_provider")
def test_kubernetes_connection_test_rejects_invalid_kubeconfig(
self, mock_return_prowler_provider
):
provider = MagicMock()
provider.provider = Provider.ProviderChoices.KUBERNETES.value
provider.uid = "provider_uid"
provider.secret.secret = {"kubeconfig_content": TOKEN_FILE_KUBECONFIG}
connection = prowler_provider_connection_test(provider)
assert connection.is_connected is False
assert (
str(connection.error) == KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
)
mock_return_prowler_provider.return_value.test_connection.assert_not_called()
@patch("api.utils.return_prowler_provider")
def test_kubernetes_connection_test_with_valid_kubeconfig(
self, mock_return_prowler_provider
):
provider = MagicMock()
provider.provider = Provider.ProviderChoices.KUBERNETES.value
provider.uid = "provider_uid"
provider.secret.secret = {"kubeconfig_content": VALID_KUBECONFIG}
prowler_provider_connection_test(provider)
mock_return_prowler_provider.return_value.test_connection.assert_called_once_with(
kubeconfig_content=VALID_KUBECONFIG,
provider_id="provider_uid",
raise_on_exception=False,
)
@pytest.mark.django_db
@patch("api.utils.return_prowler_provider")
def test_prowler_provider_connection_test_without_secret(
@@ -405,10 +468,6 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.GOOGLEWORKSPACE.value,
{},
),
(
Provider.ProviderChoices.KUBERNETES.value,
{"context": "provider_uid"},
),
(
Provider.ProviderChoices.M365.value,
{},
@@ -459,6 +518,39 @@ class TestGetProwlerProviderKwargs:
expected_result = {**secret_dict, **expected_extra_kwargs}
assert result == expected_result
def test_get_prowler_provider_kwargs_kubernetes_valid_kubeconfig(self):
provider = MagicMock()
provider.provider = Provider.ProviderChoices.KUBERNETES.value
provider.uid = "provider_uid"
provider.secret.secret = {"kubeconfig_content": VALID_KUBECONFIG}
assert get_prowler_provider_kwargs(provider) == {
"kubeconfig_content": VALID_KUBECONFIG,
"context": "provider_uid",
}
@pytest.mark.parametrize(
"secret",
[
{"kubeconfig_content": TOKEN_FILE_KUBECONFIG},
{"kubeconfig_content": EXEC_KUBECONFIG},
{"kubeconfig_content": "[]"},
{"key": "value"},
],
)
def test_get_prowler_provider_kwargs_kubernetes_rejects_invalid_kubeconfig(
self, secret
):
provider = MagicMock()
provider.provider = Provider.ProviderChoices.KUBERNETES.value
provider.uid = "provider_uid"
provider.secret.secret = secret
with pytest.raises(ValidationError) as exc_info:
get_prowler_provider_kwargs(provider)
assert "/etc/passwd" not in str(exc_info.value)
def test_get_prowler_provider_kwargs_oraclecloud_maps_region_to_home_region(
self,
):
+140
View File
@@ -63,6 +63,7 @@ from api.models import (
from api.rbac.permissions import TASK_REVOKE_PERMISSIONS
from api.rls import Tenant
from api.uuid_utils import datetime_to_uuid7
from api.v1.serializers import MembershipIncludeSerializer
from api.v1.views import (
ComplianceOverviewViewSet,
CustomSAMLLoginView,
@@ -133,6 +134,145 @@ class TestUserViewSet:
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["email"] == create_test_user.email
@staticmethod
def _shared_user_in_two_tenants(tenant_a, tenant_b):
shared_user = User.objects.create_user(
name="shared_user",
email=f"shared-user-{uuid4()}@prowler.com",
password="TmpPass123@",
)
Membership.objects.create(user=shared_user, tenant=tenant_a)
Membership.objects.create(user=shared_user, tenant=tenant_b)
role_a = Role.objects.create(name="role-a", tenant=tenant_a)
role_b = Role.objects.create(name="role-b", tenant=tenant_b)
UserRoleRelationship.objects.create(
user=shared_user, role=role_a, tenant=tenant_a
)
UserRoleRelationship.objects.create(
user=shared_user, role=role_b, tenant=tenant_b
)
return shared_user, role_a, role_b
@staticmethod
def _user_in_two_tenants_with_unique_roles(tenant_a, tenant_b):
suffix = uuid4()
user = User.objects.create_user(
name="scaled_user",
email=f"scaled-user-{suffix}@prowler.com",
password="TmpPass123@",
)
for tenant in (tenant_a, tenant_b):
Membership.objects.create(user=user, tenant=tenant)
role = Role.objects.create(name=f"role-{tenant.id}-{suffix}", tenant=tenant)
UserRoleRelationship.objects.create(user=user, role=role, tenant=tenant)
return user
def test_users_list_query_count_does_not_grow_with_user_count(
self, authenticated_client, tenants_fixture
):
tenant_a, tenant_b, _ = tenants_fixture
self._user_in_two_tenants_with_unique_roles(tenant_a, tenant_b)
params = {"include": "memberships,roles"}
with CaptureQueriesContext(connection) as baseline:
response = authenticated_client.get(reverse("user-list"), params)
assert response.status_code == status.HTTP_200_OK
baseline_users = len(response.json()["data"])
for _ in range(3):
self._user_in_two_tenants_with_unique_roles(tenant_a, tenant_b)
with CaptureQueriesContext(connection) as scaled:
response = authenticated_client.get(reverse("user-list"), params)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) > baseline_users
assert len(scaled.captured_queries) == len(baseline.captured_queries), (
"Tenant-scoped relations are re-querying per user: "
f"{len(baseline.captured_queries)} queries for {baseline_users} users "
f"vs {len(scaled.captured_queries)} for "
f"{len(response.json()['data'])}. The prefetch is being discarded."
)
def test_users_retrieve_scopes_relations_to_active_tenant(
self, authenticated_client, tenants_fixture
):
tenant_a, tenant_b, _ = tenants_fixture
shared_user, role_a, role_b = self._shared_user_in_two_tenants(
tenant_a, tenant_b
)
response = authenticated_client.get(
reverse("user-detail", kwargs={"pk": shared_user.id}),
{"include": "memberships,roles,memberships.tenant"},
)
assert response.status_code == status.HTTP_200_OK
payload = response.json()
relationships = payload["data"]["relationships"]
assert len(relationships["memberships"]["data"]) == 1
assert [item["id"] for item in relationships["roles"]["data"]] == [
str(role_a.id)
]
included = payload["included"]
included_memberships = [i for i in included if i["type"] == "memberships"]
assert len(included_memberships) == 1
assert included_memberships[0]["relationships"]["tenant"]["data"]["id"] == str(
tenant_a.id
)
assert {i["id"] for i in included if i["type"] == "roles"} == {str(role_a.id)}
assert {
i["attributes"]["name"] for i in included if i["type"] == "tenants"
} == {tenant_a.name}
body = response.content.decode()
assert tenant_b.name not in body
assert str(tenant_b.id) not in body
assert str(role_b.id) not in body
def test_users_me_keeps_own_memberships_and_scopes_roles(
self, authenticated_client, create_test_user, tenants_fixture
):
tenant_a, tenant_b, _ = tenants_fixture
response = authenticated_client.get(
reverse("user-me"), {"include": "memberships,roles,memberships.tenant"}
)
assert response.status_code == status.HTTP_200_OK
payload = response.json()
relationships = payload["data"]["relationships"]
assert len(relationships["memberships"]["data"]) == 2
assert {
i["attributes"]["name"]
for i in payload["included"]
if i["type"] == "tenants"
} == {tenant_a.name, tenant_b.name}
active_roles = create_test_user.roles.filter(tenant_id=tenant_a.id)
assert {i["id"] for i in relationships["roles"]["data"]} == {
str(role.id) for role in active_roles
}
def test_membership_include_serializer_hides_tenant_of_non_member(
self, create_test_user, tenants_fixture
):
tenant_a, _, isolated_tenant = tenants_fixture
outsider = User.objects.create_user(
name="outsider",
email=f"outsider-{uuid4()}@prowler.com",
password="TmpPass123@",
)
foreign_membership = Membership.objects.create(
user=outsider, tenant=isolated_tenant
)
own_membership = Membership.objects.get(user=create_test_user, tenant=tenant_a)
request = SimpleNamespace(user=create_test_user, tenant_id=str(tenant_a.id))
serializer = MembershipIncludeSerializer(context={"request": request})
assert serializer.get_tenant(own_membership) == tenant_a
assert serializer.get_tenant(foreign_membership) is None
def test_users_create(self, client):
valid_user_payload = {
"name": "test",
+19 -1
View File
@@ -17,7 +17,7 @@ from api.models import (
Role,
UserRoleRelationship,
)
from api.v1.serializers import FindingMetadataSerializer
from api.v1.serializers import FindingMetadataSerializer, KubernetesProviderSecret
from django.contrib.postgres.aggregates import ArrayAgg
from django.db import transaction
from django.db.models import Subquery
@@ -201,6 +201,13 @@ def return_prowler_provider(
return prowler_provider
def _validate_kubernetes_secret(secret: dict) -> None:
"""Re-validate a stored kubeconfig at use time; errors never echo its content."""
KubernetesProviderSecret(
data={"kubeconfig_content": secret.get("kubeconfig_content")}
).is_valid(raise_exception=True)
def get_prowler_provider_kwargs(
provider: Provider, mutelist_processor: Processor | None = None
) -> dict:
@@ -225,6 +232,7 @@ def get_prowler_provider_kwargs(
"project_ids": [provider.uid],
}
elif provider.provider == Provider.ProviderChoices.KUBERNETES.value:
_validate_kubernetes_secret(prowler_provider_kwargs)
prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid}
elif provider.provider == Provider.ProviderChoices.GITHUB.value:
if provider.uid:
@@ -392,6 +400,16 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
except Provider.secret.RelatedObjectDoesNotExist as secret_error:
return Connection(is_connected=False, error=secret_error)
if provider.provider == Provider.ProviderChoices.KUBERNETES.value:
# A rejected stored kubeconfig is a failed connection, not a task error.
try:
_validate_kubernetes_secret(prowler_provider_kwargs)
except ValidationError as validation_error:
return Connection(
is_connected=False,
error=Exception(validation_error.detail["kubeconfig_content"][0]),
)
# For IaC provider, construct the kwargs properly for test_connection
if provider.provider == Provider.ProviderChoices.IAC.value:
# Don't pass repository_url from secret, use scan_repository_url with the UID
@@ -214,7 +214,8 @@ from rest_framework_json_api import serializers
"kubeconfig_content": {
"type": "string",
"description": "The content of the Kubernetes kubeconfig file, encoded as a string. "
"Kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.",
"Only inline credentials are supported: token, username/password, or client-certificate-data with client-key-data for users, and certificate-authority-data for clusters. "
"File-path and auth-provider kubeconfig fields are not supported.",
}
},
"required": ["kubeconfig_content"],
+109 -34
View File
@@ -377,27 +377,48 @@ class UserSerializer(BaseModelSerializerV1):
def _can_view_relationships(self, instance) -> bool:
"""Allow self to view own relationships. Require manage_account to view others."""
role = self.context.get("role")
return self._is_self(instance) or bool(role and role.manage_account)
def _is_self(self, instance) -> bool:
request = self.context.get("request")
is_self = bool(
return bool(
request
and getattr(request, "user", None)
and getattr(instance, "id", None) == request.user.id
)
return is_self or (role and role.manage_account)
def _active_tenant_id(self):
return getattr(self.context.get("request"), "tenant_id", None)
@staticmethod
def _in_tenant(related_manager, tenant_id):
"""Filter a prefetched related manager in memory.
``.filter()`` clones the queryset without its result cache, so it would
discard the viewset's prefetch and re-query once per serialized user.
"""
return [
item
for item in related_manager.all()
if str(item.tenant_id) == str(tenant_id)
]
def get_roles(self, instance):
return (
instance.roles.all()
if self._can_view_relationships(instance)
else Role.objects.none()
)
tenant_id = self._active_tenant_id()
if not tenant_id or not self._can_view_relationships(instance):
return Role.objects.none()
return self._in_tenant(instance.roles, tenant_id)
def get_memberships(self, instance):
return (
instance.memberships.all()
if self._can_view_relationships(instance)
else Membership.objects.none()
)
if not self._can_view_relationships(instance):
return Membership.objects.none()
# Own memberships span tenants on purpose: the tenant switcher reads them.
if self._is_self(instance):
return instance.memberships.all()
tenant_id = self._active_tenant_id()
if not tenant_id:
return Membership.objects.none()
return self._in_tenant(instance.memberships, tenant_id)
class UserMeSerializer(UserSerializer):
@@ -731,11 +752,32 @@ class MembershipIncludeSerializer(serializers.ModelSerializer):
included_serializers = {"tenant": "api.v1.serializers.TenantIncludeSerializer"}
_REQUESTER_TENANTS_ATTR = "_prowler_requester_tenants"
def _requester_tenants(self):
"""Tenants the requester belongs to, resolved once per request.
Memoised on the request because DRF builds a serializer instance per
included object, so a per-instance cache would still query per membership.
"""
request = self.context.get("request")
user = getattr(request, "user", None)
if user is None:
return {}
tenants = getattr(request, self._REQUESTER_TENANTS_ATTR, None)
if tenants is None:
tenants = {
tenant.id: tenant
for tenant in Tenant.objects.using(MainRouter.admin_db).filter(
membership__user_id=user.id
)
}
setattr(request, self._REQUESTER_TENANTS_ATTR, tenants)
return tenants
def get_tenant(self, instance):
try:
return Tenant.objects.using(MainRouter.admin_db).get(id=instance.tenant_id)
except Tenant.DoesNotExist:
return None
"""Resolve the tenant only when the requester is a member of it."""
return self._requester_tenants().get(instance.tenant_id)
# Provider Groups
@@ -1659,14 +1701,43 @@ class FindingMetadataSerializer(BaseSerializerV1):
# Provider secrets
KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR = (
"Kubernetes kubeconfig command-based authentication is not supported in "
"Prowler Cloud for security reasons."
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR = (
"Only inline Kubernetes credentials are supported. "
"For user authentication use token, username/password, or client-certificate-data with client-key-data; "
"clusters may use certificate-authority-data. "
"File-path fields (tokenFile, client-certificate, client-key, certificate-authority), "
"command-based authentication (exec, cmd-path), auth-provider directives, and proxy URLs are not supported."
)
KUBERNETES_KUBECONFIG_INVALID_ERROR = "Invalid Kubernetes kubeconfig content."
def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool:
KUBECONFIG_ALLOWED_USER_KEYS = frozenset(
{
"token",
"username",
"password",
"client-certificate-data",
"client-key-data",
"as",
"as-uid",
"as-groups",
"as-user-extra",
}
)
KUBECONFIG_ALLOWED_CLUSTER_KEYS = frozenset(
{
"server",
"certificate-authority-data",
"insecure-skip-tls-verify",
"tls-server-name",
"disable-compression",
"extensions",
}
)
def kubeconfig_is_inline_only_credentials(kubeconfig: dict) -> bool:
"""Accept only inline credentials; reject file-reference and auth-provider fields."""
users = kubeconfig.get("users", [])
if not isinstance(users, list):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
@@ -1679,21 +1750,25 @@ def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool:
if not isinstance(user, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if "exec" in user:
return True
if any(key not in KUBECONFIG_ALLOWED_USER_KEYS for key in user):
return False
auth_provider = user.get("auth-provider", {})
if not isinstance(auth_provider, dict):
continue
clusters = kubeconfig.get("clusters", [])
if not isinstance(clusters, list):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
auth_provider_config = auth_provider.get("config", {})
if not isinstance(auth_provider_config, dict):
continue
for cluster_entry in clusters:
if not isinstance(cluster_entry, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if "cmd-path" in auth_provider_config:
return True
cluster = cluster_entry.get("cluster", {})
if not isinstance(cluster, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
return False
if any(key not in KUBECONFIG_ALLOWED_CLUSTER_KEYS for key in cluster):
return False
return True
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
@@ -1876,7 +1951,7 @@ class MongoDBAtlasProviderSecret(serializers.Serializer):
class KubernetesProviderSecret(serializers.Serializer):
kubeconfig_content = serializers.CharField()
kubeconfig_content = serializers.CharField(trim_whitespace=False)
def validate_kubeconfig_content(self, kubeconfig_content):
try:
@@ -1889,9 +1964,9 @@ class KubernetesProviderSecret(serializers.Serializer):
if not isinstance(kubeconfig, dict):
raise serializers.ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if kubeconfig_contains_unsupported_command_auth(kubeconfig):
if not kubeconfig_is_inline_only_credentials(kubeconfig):
raise serializers.ValidationError(
KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
)
return kubeconfig_content
+39 -3
View File
@@ -3,7 +3,8 @@ from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
from api.models import Integration, LighthouseConfiguration, Provider
from api.models import Integration, LighthouseConfiguration, Provider, ProviderSecret
from api.v1.serializers import KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
from tasks.jobs.connection import (
check_integration_connection,
check_lighthouse_connection,
@@ -65,17 +66,52 @@ def test_check_provider_connection_exception(
mock_provider_connection_test.return_value = MagicMock()
mock_provider_connection_test.return_value.is_connected = False
mock_provider_connection_test.return_value.error = Exception()
mock_provider_connection_test.return_value.error = Exception(
"Unable to assume role"
)
result = check_provider_connection(provider_id="provider_id")
assert result["connected"] is False
assert result["error"] is not None
assert result["error"] == "Unable to assume role"
mock_provider_instance.save.assert_called_once()
assert mock_provider_instance.connected is False
@pytest.mark.django_db
def test_check_provider_connection_with_stored_non_inline_kubeconfig(
kubernetes_provider,
):
"""A kubeconfig stored before the inline-only rule marks the provider as disconnected."""
kubernetes_provider.connected = True
kubernetes_provider.save()
ProviderSecret.objects.create(
tenant_id=kubernetes_provider.tenant_id,
provider=kubernetes_provider,
secret_type=ProviderSecret.TypeChoices.STATIC,
secret={
"kubeconfig_content": (
"apiVersion: v1\n"
"kind: Config\n"
"users:\n"
"- name: test-user\n"
" user:\n"
" tokenFile: /etc/passwd\n"
)
},
)
result = check_provider_connection(provider_id=str(kubernetes_provider.id))
kubernetes_provider.refresh_from_db()
assert result == {
"connected": False,
"error": KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}
assert kubernetes_provider.connected is False
@pytest.mark.parametrize(
"lighthouse_data",
[
Generated
+50 -49
View File
@@ -138,7 +138,7 @@ constraints = [
{ name = "dill", specifier = "==0.4.1" },
{ name = "distro", specifier = "==1.9.0" },
{ name = "dj-rest-auth", specifier = "==7.0.1" },
{ name = "django", specifier = "==5.1.15" },
{ name = "django", specifier = "==5.2.17" },
{ name = "django-allauth", specifier = "==65.15.0" },
{ name = "django-celery-beat", specifier = "==2.9.0" },
{ name = "django-celery-results", specifier = "==2.6.0" },
@@ -162,7 +162,7 @@ constraints = [
{ name = "drf-simple-apikey", specifier = "==2.2.1" },
{ name = "drf-spectacular", specifier = "==0.27.2" },
{ name = "drf-spectacular-jsonapi", specifier = "==0.5.1" },
{ name = "dulwich", specifier = "==1.2.5" },
{ name = "dulwich", specifier = "==1.2.9" },
{ name = "duo-client", specifier = "==5.5.0" },
{ name = "durationpy", specifier = "==0.10" },
{ name = "email-validator", specifier = "==2.2.0" },
@@ -355,7 +355,7 @@ constraints = [
{ name = "tzdata", specifier = "==2025.3" },
{ name = "tzlocal", specifier = "==5.3.1" },
{ name = "uritemplate", specifier = "==4.2.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "urllib3", specifier = "==2.8.0" },
{ name = "uuid6", specifier = "==2024.7.10" },
{ name = "uvicorn", specifier = "==0.49.0" },
{ name = "uvloop", specifier = "==0.22.1" },
@@ -378,7 +378,7 @@ constraints = [
overrides = [
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "dulwich", specifier = "==1.2.5" },
{ name = "dulwich", specifier = "==1.2.9" },
{ name = "microsoft-kiota-abstractions", specifier = "==1.9.10" },
{ name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" },
{ name = "microsoft-kiota-http", specifier = "==1.9.10" },
@@ -388,6 +388,7 @@ overrides = [
{ name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
{ name = "okta", specifier = "==3.4.2" },
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.14.0" },
{ name = "pymongo", specifier = "==4.18.2" },
]
[[package]]
@@ -2329,16 +2330,16 @@ with-social = [
[[package]]
name = "django"
version = "5.1.15"
version = "5.2.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/45/1ac68964193cfcc0b0912a0f68025d5bdb54f71ba7b8716e85b959874bd0/django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947", size = 10719662, upload-time = "2025-12-02T14:01:31.931Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/79/372e091f0eba4ddb8228245ccd1baaa140e9658711f5e3a0056e540b4c1e/django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", size = 8260901, upload-time = "2025-12-02T14:01:27.352Z" },
{ url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" },
]
[[package]]
@@ -2667,27 +2668,27 @@ wheels = [
[[package]]
name = "dulwich"
version = "1.2.5"
version = "1.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/85/ceb8ecff5cdeee4ceeebb86b599476dee559041dacc6c2c50cc0d4711549/dulwich-1.2.5.tar.gz", hash = "sha256:0395b2c8924c3424bafe2d9c1edd5348cc4b21ce9c1d6655bf01f9a5c47164c8", size = 1253230, upload-time = "2026-05-28T22:27:55.17Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/17/0a0fa2b998cd9a852b1aa901e3f6145f13a4334f64b91564847e5880968d/dulwich-1.2.9.tar.gz", hash = "sha256:b395466b78eb5c36baf34381b8b831fd26da8a44b428a2eea1d57ba53f68a234", size = 1309797, upload-time = "2026-07-06T20:57:23.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/4a/654ae1671610fdf6b65a64586ad67ddd8550d4d08a632b2a4b9614754b6d/dulwich-1.2.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:556593fd11637f80f6018bee1916b1a84f5b420423b470ebb3f1a782ad6ef081", size = 1399277, upload-time = "2026-05-28T22:27:00.801Z" },
{ url = "https://files.pythonhosted.org/packages/85/d8/06ee3bc8eded4bd7adf8adf0c9ea5f19bf96f7e5e626bfaf7311cde4208a/dulwich-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a70477c991e96cfe8fdd7c866e7251faf71b38bfeb51d6f27554c9cce1caabf3", size = 1382310, upload-time = "2026-05-28T22:27:02.216Z" },
{ url = "https://files.pythonhosted.org/packages/07/17/a03adf50b9095f9f5d863393f21d585dea39bdc4fdf60788ff3a9407a512/dulwich-1.2.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9008ef25cabd379cda4fa86000fc38ca14b72afe17db798a8c85c0b2b7ce4d1e", size = 1470993, upload-time = "2026-05-28T22:27:04.075Z" },
{ url = "https://files.pythonhosted.org/packages/60/58/1dc352d2a5e80befe4338af7208febb44bcfd7496b0dde5ac6dacb07b031/dulwich-1.2.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a5549f4afc973e0a15ea6b0244d57f848d3f3ee13dac557eb311024aebebf128", size = 1497820, upload-time = "2026-05-28T22:27:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a8/e058959a87e7df7753b112ef66a43ccbc57338c1bbdc23a0edf3833396df/dulwich-1.2.5-cp311-cp311-win32.whl", hash = "sha256:5108acead814d1de8b6262d6d8fb90af7e82f5a4d83788b6b48e39d01800a92f", size = 1066549, upload-time = "2026-05-28T22:27:06.832Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/ff0b444f686718635348986bd73dfce42e947912417893de35de399b878b/dulwich-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:5e067b7feceb7034bc99e7c7143a704f1d97d4be7027d9a0aa5a83c0657ff091", size = 1079481, upload-time = "2026-05-28T22:27:08.33Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/4f75770bbe5521cac61c4820ef46d4fbf8c2175d3519ba3d0378d4ba798e/dulwich-1.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:701a9ecf7a8a44f5e2459e46befa93530cf36a8b1ae3140aefc007db1d7d0207", size = 1396522, upload-time = "2026-05-28T22:27:09.997Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b1/c07c347681c0cf6acd4b189bf6e8d6207c71a1347b7a1e865eb40faa46b9/dulwich-1.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f90d68bfa97c4ca71de7507984365aefe27b6d248cb28dc99644d0f3ae8c60b", size = 1334826, upload-time = "2026-05-28T22:27:11.582Z" },
{ url = "https://files.pythonhosted.org/packages/13/80/6818eb7ce492e18ab2efa92ab901d173b4b0b159e5681c1424f329600c40/dulwich-1.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00b54a1d56ddbacdd8eadd6d4787a51b3a05fefa30eadbf9165fd283a00b90ed", size = 1416616, upload-time = "2026-05-28T22:27:13.195Z" },
{ url = "https://files.pythonhosted.org/packages/14/a7/9790e60d19870f6554f7583722bb324c1355784316f20aeda1c0b5b1491a/dulwich-1.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8f7ea8f47e38e5b0de3fab97e07e9c9161ffddc90b3964512cab2b7749df4e6", size = 1441354, upload-time = "2026-05-28T22:27:14.683Z" },
{ url = "https://files.pythonhosted.org/packages/91/44/0ea8a69c24aa1254ff5996d682eae2eab287d471b937dcdb26d9ea9720b4/dulwich-1.2.5-cp312-cp312-win32.whl", hash = "sha256:8929134acf4ff967203df7600b38535f9b5b590462067a7e30dbce01acb97af9", size = 1017058, upload-time = "2026-05-28T22:27:16.121Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/2fcddda7faec3bae52db7c64bfcb5dc756f597f33fae90e8d4e4b4d3b39b/dulwich-1.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:9693d2c9e226b2ea855c1dc3a87e2f4d972f7523fc0f7924e5997e9f4c23d97f", size = 1031731, upload-time = "2026-05-28T22:27:17.633Z" },
{ url = "https://files.pythonhosted.org/packages/07/4b/4a18a59ad230581cd0ef460e96001f90762e566dc2dfdba22aa358eb5a0e/dulwich-1.2.5-py3-none-any.whl", hash = "sha256:1679b376433a0fc7f36586afda1d4ed7427afa7a79d4bf17e5014474eea69fa4", size = 686745, upload-time = "2026-05-28T22:27:53.695Z" },
{ url = "https://files.pythonhosted.org/packages/95/70/0345eb2b07a9b26337d7e9ee2c0fc634be81de9969d683b72eca04878f72/dulwich-1.2.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b01fd4d8bd4fdd2bd99eb465e101b4fac2b50436e317b971da7ac3a1bd3a83d8", size = 1378231, upload-time = "2026-07-06T20:56:20.301Z" },
{ url = "https://files.pythonhosted.org/packages/56/dd/a699facf7371cb26233ea1f659ff172995cc06a999ba07992d2762a7dc1f/dulwich-1.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e92f76f0048b076ef692094d28645c5e0f65dae1d5e393fbb2e8db2edc017cd3", size = 1371812, upload-time = "2026-07-06T20:56:21.958Z" },
{ url = "https://files.pythonhosted.org/packages/35/02/73baf09f61b66431adb1cd01a9fbe748369a62814b6e27362daaaa924f04/dulwich-1.2.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ac47e02988bdcde0c23f98275908390448617d491ad5d9cb9f33cf3eb8ad512b", size = 1493888, upload-time = "2026-07-06T20:56:23.697Z" },
{ url = "https://files.pythonhosted.org/packages/48/e4/aa2dff296c4a9cd769b5a55a8de68622c1605ad39d75b741527e63117acf/dulwich-1.2.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5e6eb07fbd288059da71019f6746655cd1e136db2f1664fa3e1c81bc11973187", size = 1523051, upload-time = "2026-07-06T20:56:25.525Z" },
{ url = "https://files.pythonhosted.org/packages/24/da/73162905ffb2041a8f484e46c7bb269cb39cc984335b1e66eef23d38cb1d/dulwich-1.2.9-cp311-cp311-win32.whl", hash = "sha256:e96f5d29bf509ceb2dbf9cf4de67fe6e5d785b22f721814360563be6757e7473", size = 1043350, upload-time = "2026-07-06T20:56:27.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/e6/8e40755771b5d05c9268771f26cef8f2ba5f7a849bedcee06504d7f842ae/dulwich-1.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:255c4c3605582fe8958afeff94fc1f0883aedc37495c43a74e031b904f3bb86d", size = 1103738, upload-time = "2026-07-06T20:56:29.145Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b0/e4f14c603fa42e07d8a7f1d5a73f123a8148d52b79b3cc478797126dcdd3/dulwich-1.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d5a2896b6dc3f0c794d34b3eb1af317a43b557fefbee0099ef83d35bb6e102f", size = 1372001, upload-time = "2026-07-06T20:56:30.605Z" },
{ url = "https://files.pythonhosted.org/packages/c9/b7/11c40f1c3ce304e7c2e0e759c589430f043a7967689e61ecb0142ad1ebdc/dulwich-1.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30e72110630e2990d11770227191dfa730b7c045030f818eca5803c6b89bbc5b", size = 1356130, upload-time = "2026-07-06T20:56:32.176Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/10d61b784c295a4567615579272d91ede81793c72ead207484c0ed845ddd/dulwich-1.2.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5c636fcb33c4db5495a2dc7686cf6a26cf537060a859982e104dcf67635ea7f7", size = 1437923, upload-time = "2026-07-06T20:56:33.741Z" },
{ url = "https://files.pythonhosted.org/packages/6e/68/3073e2737e2c3f299e736490ec2e8b5864593858b9d08f36fbcbca80a862/dulwich-1.2.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1aa1b55f59a046fc8f25a67560473c789b7ec113b85c6a08021aae10c6f78945", size = 1463517, upload-time = "2026-07-06T20:56:35.812Z" },
{ url = "https://files.pythonhosted.org/packages/f2/61/f96c0da72b08bfbc4e38571374c3000dee13e6d72dd06f25281d36d5b0b7/dulwich-1.2.9-cp312-cp312-win32.whl", hash = "sha256:e27a06f9f71275d712b616ad23008df5f2dcf54a06cb8e2a24be8819ee1ce13a", size = 1082071, upload-time = "2026-07-06T20:56:38.139Z" },
{ url = "https://files.pythonhosted.org/packages/38/96/567f32885e780aaa7441f9f5c6f162e7271d149e27d3cb661caf939cd671/dulwich-1.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:81e1005ffc5d5c872bfc87ac3442f722bce27ba10e435543d5302853c34abf56", size = 1053612, upload-time = "2026-07-06T20:56:39.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/f9/a6677c0e698b5b781d663eb394bd15cc69c4a270b11cdcc128076753d75c/dulwich-1.2.9-py3-none-any.whl", hash = "sha256:79ff0ff35c4e17ccd58f9bfb23f4f8d416b0b09337b8c99ee3b4f10459bb0d68", size = 711459, upload-time = "2026-07-06T20:57:21.516Z" },
]
[[package]]
@@ -4202,7 +4203,7 @@ wheels = [
[package.optional-dependencies]
broker = [
{ name = "pymsalruntime", marker = "sys_platform == 'win32'" },
{ name = "pymsalruntime" },
]
[[package]]
@@ -5012,7 +5013,7 @@ requires-dist = [
{ name = "celery", specifier = "==5.6.2" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "dj-rest-auth", extras = ["with-social", "jwt"], specifier = "==7.0.1" },
{ name = "django", specifier = "==5.1.15" },
{ name = "django", specifier = "==5.2.17" },
{ name = "django-allauth", extras = ["saml"], specifier = "==65.15.0" },
{ name = "django-celery-beat", specifier = "==2.9.0" },
{ name = "django-celery-results", specifier = "==2.6.0" },
@@ -5364,33 +5365,33 @@ wheels = [
[[package]]
name = "pymongo"
version = "4.15.1"
version = "4.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" },
{ url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" },
{ url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" },
{ url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" },
{ url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" },
{ url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" },
{ url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" },
{ url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" },
{ url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" },
{ url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" },
{ url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" },
{ url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" },
{ url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" },
{ url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" },
{ url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" },
{ url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" },
{ url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" },
{ url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" },
{ url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" },
{ url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" },
{ url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" },
{ url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" },
{ url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" },
{ url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" },
{ url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" },
{ url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" },
{ url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" },
{ url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" },
{ url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" },
{ url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" },
{ url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" },
{ url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" },
]
[[package]]
@@ -6332,11 +6333,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.7.0"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
{ url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]
@@ -25,7 +25,7 @@ description: 'Onboard Kubernetes clusters to Prowler and scan for security misco
For Kubernetes, Prowler Cloud uses a `kubeconfig` file to authenticate. Paste the contents of your `kubeconfig` file into the `Kubeconfig content` field.
<Note>
Kubeconfigs that use `users[].user.exec` authentication are not supported in Prowler Cloud or Prowler Local Server. For security reasons, Prowler Cloud does not run commands declared by uploaded kubeconfigs. Use kubeconfig credentials that do not rely on `exec` authentication, such as the ServiceAccount token flow documented below.
Only inline kubeconfig credentials are supported: `token`, `username`/`password`, or `client-certificate-data` with `client-key-data` for users, and `certificate-authority-data` for clusters. Kubeconfigs that reference external files (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), run commands (`exec`, `cmd-path`), use `auth-provider` directives, set a proxy URL, or include unsupported user or cluster fields are rejected. Impersonation fields (`as`, `as-uid`, `as-groups`, `as-user-extra`) are accepted but not applied, so Prowler connects with the identity of the user credentials. Use inline credentials such as the ServiceAccount token flow documented below.
</Note>
By default, the `kubeconfig` file is located at `~/.kube/config`.
@@ -97,6 +97,8 @@ If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these
kubectl config set-context <CONTEXT_NAME> --user=prowler-sa
```
Prowler validates the entire kubeconfig file and requires it to contain only the ServiceAccount user and its cluster entry with certificate-authority-data; remove any exec entries from EKS, GKE, or AKS or Prowler will reject the file.
Replace `<SA_TOKEN>` with the generated token and `<CONTEXT_NAME>` with your KubeConfig Context Name of your EKS, GKE or AKS cluster.
4. Add the modified `kubeconfig` in Prowler Cloud and test the connection.
+6 -20
View File
@@ -25,34 +25,20 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# =============================================================================
# Final stage - Minimal runtime environment
# =============================================================================
FROM python:3.13.14-alpine3.23@sha256:9fdbf2e3e82628351513560b121e2ee6ce31cac212be9e070c5a5e2769fb5e76
FROM python:3.13.16-alpine3.23@sha256:07b20c774a247152e74f622034c1482495d299e070eec620ddff13e01264290b
LABEL maintainer="https://github.com/prowler-cloud"
# High CVEs fixed in Alpine 3.23 but not yet in the pinned base image:
# sqlite-libs 3.53.4-r0 CVE-2026-11822, CVE-2026-11824 (image ships 3.51.2-r0)
# libcrypto3/libssl3 3.5.8-r0 CVE-2026-14456 (image ships 3.5.7-r0)
# libuuid 2.41.6-r1 CVE-2026-53612, -53613, -53614, -76642, -78408, -78410
# (image ships 2.41.4-r0; -78408 is the one that needs -r1 rather than -r0)
# The base image pins python 3.13.14, which has not been rebuilt since those
# packages were published, so the upgrade is taken here rather than by moving
# the pin -- the newest published python:3.13-alpine3.23 carries the same
# vulnerable versions. libcrypto3 and libssl3 are both built from openssl and
# are flagged separately, so both are named.
# `>=` rather than `=`: Alpine keeps only the newest build of a package in a
# branch's index, so an exact pin breaks this build the day one of these is
# superseded. Drop an entry once the base image ships that version or later.
RUN apk add --no-cache --upgrade \
"sqlite-libs>=3.53.4-r0" \
"libcrypto3>=3.5.8-r0" \
"libssl3>=3.5.8-r0" \
"libuuid>=2.41.6-r1"
# Create non-root user for security
# Using specific UID/GID for consistency across environments
RUN addgroup -g 1001 prowler && \
adduser -D -u 1001 -G prowler prowler
# pip is build-only; the entrypoint runs the venv directly.
RUN rm -rf /usr/local/lib/python3.13/site-packages/pip \
/usr/local/lib/python3.13/site-packages/pip-*.dist-info \
/usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.13
WORKDIR /app
USER prowler
@@ -0,0 +1 @@
Python from 3.13.14 to 3.13.16 in the MCP container image, patching CVE-2026-19445 and CVE-2026-19553
@@ -0,0 +1 @@
AWS Organizations metadata is fetched from the partition of the audited account, using the region that answered the STS bootstrap, instead of the commercial `us-east-1` endpoint a session without a region resolves to, which timed out in GovCloud
@@ -0,0 +1 @@
`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx
@@ -0,0 +1 @@
`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749
@@ -0,0 +1 @@
Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the SDK container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy
@@ -0,0 +1 @@
`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689
+8 -3
View File
@@ -390,7 +390,7 @@ class AwsProvider(Provider):
)
self._organizations_metadata = self.get_organizations_info(
aws_organizations_session, self._identity.account
aws_organizations_session, self._identity.account, sts_region
)
# Fallback to the original (pre-assume) session when no explicit
@@ -412,7 +412,7 @@ class AwsProvider(Provider):
"Retrying AWS Organizations metadata retrieval with the original session"
)
self._organizations_metadata = self.get_organizations_info(
self._session.original_session, self._identity.account
self._session.original_session, self._identity.account, sts_region
)
########
@@ -508,7 +508,10 @@ class AwsProvider(Provider):
# TODO: This can be moved to another class since it doesn't need self
def get_organizations_info(
self, organizations_session: Session, aws_account_id: str
self,
organizations_session: Session,
aws_account_id: str,
region: str | None = None,
) -> AWSOrganizationsInfo:
"""
get_organizations_info returns a AWSOrganizationsInfo object if the account to be audited is a delegated administrator for AWS Organizations or if the AWS Organizations Role ARN (--organizations-role) is passed.
@@ -516,6 +519,7 @@ class AwsProvider(Provider):
Args:
- organizations_session: needs to be a Session object with permissions to do organizations:DescribeAccount and organizations:ListTagsForResource.
- aws_account_id: is the AWS Account ID from which we want to get the AWS Organizations account metadata
- region: a region of the audited partition, so the client reaches that partition's Organizations endpoint
Returns:
- AWSOrganizationsInfo object with the AWS Organizations metadata for the account to be audited.
@@ -529,6 +533,7 @@ class AwsProvider(Provider):
get_organizations_metadata(
aws_account_id=aws_account_id,
session=organizations_session,
region=region,
)
)
@@ -42,9 +42,11 @@ def _get_ou_metadata(organizations_client, account_id):
def get_organizations_metadata(
aws_account_id: str,
session: session.Session,
region: str | None = None,
) -> tuple[dict, dict, dict]:
try:
organizations_client = session.client("organizations")
# Without a region botocore resolves the commercial endpoint, whatever the partition
organizations_client = session.client("organizations", region_name=region)
organizations_metadata = organizations_client.describe_account(
AccountId=aws_account_id
+9 -3
View File
@@ -76,7 +76,7 @@ dependencies = [
"dash==3.1.1",
"dash-bootstrap-components==2.0.3",
"defusedxml==0.7.1",
"dulwich==1.2.5",
"dulwich==1.2.9",
"google-api-python-client==2.163.0",
"google-auth-httplib2==0.2.0",
"jsonschema==4.23.0",
@@ -387,7 +387,7 @@ constraint-dependencies = [
"typing-inspection==0.4.2",
"tzdata==2026.2",
"uritemplate==4.2.0",
"urllib3==2.7.0",
"urllib3==2.8.0",
"vulture==2.14",
"websocket-client==1.9.0",
"werkzeug==3.1.8",
@@ -398,7 +398,13 @@ constraint-dependencies = [
"zipp==3.23.1",
"zstd==1.5.7.2"
]
override-dependencies = ["okta==3.4.2"]
override-dependencies = [
"okta==3.4.2",
# huaweicloudsdkcore pins pymongo==4.15.1 exactly (still so in 3.1.216) and only uses
# its bson types. The container installs from this lock, so the override reaches the
# image; a pip install of the wheel still resolves 4.15.1 through the huawei pin.
"pymongo==4.18.2"
]
[tool.vulture]
# Suppress known false positives. The CI command only passes --exclude and
+38 -4
View File
@@ -490,10 +490,10 @@ class TestAWSProvider:
captured_sessions = []
original_get_organizations_info = AwsProvider.get_organizations_info
def capture(self, organizations_session, aws_account_id):
def capture(self, organizations_session, aws_account_id, region=None):
captured_sessions.append(organizations_session)
return original_get_organizations_info(
self, organizations_session, aws_account_id
self, organizations_session, aws_account_id, region
)
with patch.object(AwsProvider, "get_organizations_info", capture):
@@ -513,10 +513,10 @@ class TestAWSProvider:
captured_sessions = []
original_get_organizations_info = AwsProvider.get_organizations_info
def capture(self, organizations_session, aws_account_id):
def capture(self, organizations_session, aws_account_id, region=None):
captured_sessions.append(organizations_session)
return original_get_organizations_info(
self, organizations_session, aws_account_id
self, organizations_session, aws_account_id, region
)
with patch.object(AwsProvider, "get_organizations_info", capture):
@@ -2132,6 +2132,40 @@ aws:
assert sts_regions == [AWS_REGION_GOV_CLOUD_US_WEST_1]
@mock_aws
def test_aws_provider_queries_organizations_where_validation_got_an_answer(
self, monkeypatch
):
monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION)
# No region in the session: Organizations would resolve to us-east-1
monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
monkeypatch.delenv("AWS_REGION", raising=False)
answered = AWSCallerIdentity(
user_id="test-user-id",
account=AWS_ACCOUNT_NUMBER,
arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN),
region=AWS_REGION_GOV_CLOUD_US_WEST_1,
)
organizations_regions = []
def get_organizations_metadata(aws_account_id, session, region=None):
organizations_regions.append(region)
return {}, {}, {}
with (
patch(
"prowler.providers.aws.aws_provider.AwsProvider.validate_credentials",
return_value=answered,
),
patch(
"prowler.providers.aws.aws_provider.get_organizations_metadata",
side_effect=get_organizations_metadata,
),
):
AwsProvider()
assert organizations_regions == [AWS_REGION_GOV_CLOUD_US_WEST_1]
@mock_aws
def test_test_connection_with_env_credentials(self, monkeypatch):
# Create a mock IAM user
@@ -238,6 +238,17 @@ class Test_AWS_Organizations:
assert default_config is not None
assert BOTO3_USER_AGENT_EXTRA in default_config.user_agent_extra
def test_get_organizations_metadata_uses_the_given_region(self):
# A session with no region resolves Organizations to the commercial
# global endpoint, unreachable from GovCloud
wrapper = MagicMock(wraps=boto3.Session())
get_organizations_metadata("123456789012", wrapper, region="us-gov-west-1")
wrapper.client.assert_called_once_with(
"organizations", region_name="us-gov-west-1"
)
def test_parse_organizations_metadata_with_empty_ou_metadata(self):
tags = {"Tags": []}
metadata = {
+17
View File
@@ -0,0 +1,17 @@
import { http, HttpResponse } from "msw";
// A 1×1 transparent PNG.
const PNG = Uint8Array.from(
atob(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
),
(char) => char.charCodeAt(0),
);
/** Serves a registry logo image, so a test never reaches the real registry. */
export function handleRegistryLogo(url: string) {
return http.get(
url,
() => new HttpResponse(PNG, { headers: { "Content-Type": "image/png" } }),
);
}
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import openaiSchema from "@/lib/provider-credentials/fixtures/openai-credential-schema.json";
import templateSchema from "@/lib/provider-credentials/fixtures/template-credential-schema.json";
import unionSchema from "@/lib/provider-credentials/fixtures/union-credential-schema.json";
const { fetchMock, getProviderSchemas, getAuthHeaders, revalidatePath } =
vi.hoisted(() => ({
fetchMock: vi.fn(),
@@ -136,6 +137,61 @@ describe("dynamic provider credential actions", () => {
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
describe("when the secret type's schema offers several variants", () => {
const basic = {
auth_method: "basic",
host: "api.acme.test",
username: "fixture-user",
password: "fixture-password-not-a-secret",
};
beforeEach(() => {
getProviderSchemas.mockResolvedValue({
status: "success",
providerType: "acme",
secretTypes: { static: unionSchema },
});
});
it("sends a secret that one variant accepts", async () => {
// Given
fetchMock
.mockResolvedValueOnce(response(account()))
.mockResolvedValueOnce(response({ data: { id: "saved" } }, 201));
// When
const result = await saveDynamicProviderCredentials({
...input,
secretType: "static",
secret: basic,
});
// Then
expect(result).toEqual({ status: "saved", secretId: "saved" });
expect(
JSON.parse(fetchMock.mock.calls[1][1].body).data.attributes,
).toEqual({ secret_type: "static", secret: basic });
});
it.each([
["fields of two variants", { ...basic, token: "fixture-token" }],
["another variant's discriminator", { ...basic, auth_method: "token" }],
["a missing required field", { ...basic, password: "" }],
])("does not write a secret with %s", async (_name, secret) => {
// Given
fetchMock.mockResolvedValueOnce(response(account()));
// When
const result = await saveDynamicProviderCredentials({
...input,
secretType: "static",
secret,
});
// Then
expect(result.status).toBe("invalid");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
it.each([
{ ...input, secretType: "invented" },
{ ...input, secret: { token: "" } },
@@ -4,8 +4,8 @@ import { revalidatePath } from "next/cache";
import { z } from "zod";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { parseRegistryCredentialSchema } from "@/lib/provider-credentials/provider-credential-schema";
import { validateCredentialValues } from "@/lib/provider-credentials/provider-credential-values";
import { parseRegistryCredentialVariants } from "@/lib/provider-credentials/provider-credential-schema";
import { validateCredentialVariants } from "@/lib/provider-credentials/provider-credential-values";
import { isKnownProviderType } from "@/types/providers";
import { getProviderSchemas } from "./provider-schemas";
@@ -66,11 +66,11 @@ export async function saveDynamicProviderCredentials(
!Object.hasOwn(schemas.secretTypes, secretType)
)
return { status: "schema_unavailable" };
const schema = parseRegistryCredentialSchema(
const variants = parseRegistryCredentialVariants(
schemas.secretTypes[secretType],
);
if (!schema) return { status: "schema_unavailable" };
const validated = validateCredentialValues(schema, secret);
if (!variants) return { status: "schema_unavailable" };
const validated = validateCredentialVariants(variants, secret);
if (!validated.valid)
return { status: "invalid", errors: validated.errors };
+16
View File
@@ -227,6 +227,22 @@ export async function getInstalledRegistryProviderOptions(): Promise<RegistryPro
};
}
/** Logo of each registry provider type; empty whenever Registry is out of reach. */
export async function getRegistryProviderLogos(): Promise<
Record<string, string>
> {
const access = (await auth())?.accessToken;
const permission = await evaluateRegistryProviderAccess(access);
if (!access || permission.status !== REGISTRY_ACCESS.ELIGIBLE) return {};
const result = await readRegistryProviders(access, null);
if (result.status !== "ready") return {};
return Object.fromEntries(
result.providers.flatMap(({ type, logoUrl }) =>
logoUrl?.startsWith("https://") ? [[type, logoUrl]] : [],
),
);
}
async function readCompleteRegistryCatalog(
accessToken: string,
credential: RegistryCredentialStatus | null,
@@ -22,6 +22,10 @@ const manageGroupsActionsMock = vi.hoisted(() => ({
getAllProviderGroups: vi.fn(),
}));
const registryActionsMock = vi.hoisted(() => ({
getRegistryProviderLogos: vi.fn(),
}));
vi.mock("@/actions/providers", () => providersActionsMock);
vi.mock(
"@/actions/organizations/organizations",
@@ -30,6 +34,7 @@ vi.mock(
vi.mock("@/actions/scans", () => scansActionsMock);
vi.mock("@/actions/schedules", () => schedulesActionsMock);
vi.mock("@/actions/manage-groups/manage-groups", () => manageGroupsActionsMock);
vi.mock("@/actions/registry/registry", () => registryActionsMock);
import { SearchParamsProps } from "@/types";
import { NODE_KIND } from "@/types/organizations";
@@ -1313,4 +1318,96 @@ describe("loadProvidersAccountsViewData", () => {
false,
);
});
const givenRegistryProviderPage = (
logos: Promise<Record<string, string>>,
) => {
const registryProvider = {
...providersResponse.data[1],
id: "provider-vcf",
attributes: {
...providersResponse.data[1].attributes,
provider: "vcf",
is_dynamic: true,
},
};
providersActionsMock.getProviders.mockResolvedValue({
...providersResponse,
data: [providersResponse.data[0], registryProvider],
});
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
schedulesActionsMock.getSchedules.mockResolvedValue({ data: [] });
organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({
data: [],
});
organizationsActionsMock.listOrganizationNodesSafe.mockResolvedValue({
data: [],
});
registryActionsMock.getRegistryProviderLogos.mockReturnValue(logos);
};
it("streams the logo the registry publishes for each provider type", async () => {
// Given
givenRegistryProviderPage(
Promise.resolve({
vcf: "https://media.registry.example.com/providers/vcf/logo.png",
}),
);
// When
const viewData = await loadProvidersAccountsViewData({
searchParams: {} satisfies SearchParamsProps,
isCloud: true,
});
// Then
await expect(viewData.registryLogos).resolves.toEqual({
vcf: "https://media.registry.example.com/providers/vcf/logo.png",
});
});
it("returns the provider rows without waiting for registry logos", async () => {
// Given: a registry that never answers.
givenRegistryProviderPage(new Promise(() => {}));
// When
const result = await Promise.race([
loadProvidersAccountsViewData({
searchParams: {} satisfies SearchParamsProps,
isCloud: true,
}),
new Promise<"blocked">((resolve) =>
setTimeout(() => resolve("blocked"), 200),
),
]);
// Then
expect(result).not.toBe("blocked");
if (result === "blocked") return;
expect(findProviderRow(result.rows, "provider-vcf")).toBeDefined();
});
it("does not ask the registry for logos when the page lists no registry providers", async () => {
// Given
registryActionsMock.getRegistryProviderLogos.mockClear();
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
schedulesActionsMock.getSchedules.mockResolvedValue({ data: [] });
organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({
data: [],
});
organizationsActionsMock.listOrganizationNodesSafe.mockResolvedValue({
data: [],
});
// When
const viewData = await loadProvidersAccountsViewData({
searchParams: {} satisfies SearchParamsProps,
isCloud: true,
});
// Then
await expect(viewData.registryLogos).resolves.toEqual({});
expect(registryActionsMock.getRegistryProviderLogos).not.toHaveBeenCalled();
});
});
@@ -5,6 +5,7 @@ import {
} from "@/actions/organizations/organizations";
import { getAllProviders, getProviders } from "@/actions/providers";
import { PROVIDERS_FILTER_PARAM } from "@/actions/providers/providers-filters";
import { getRegistryProviderLogos } from "@/actions/registry/registry";
import { getSchedules } from "@/actions/schedules";
import {
extractFiltersAndQuery,
@@ -142,6 +143,14 @@ const getProviderLastScanAt = (
return provider.attributes.connection.last_checked_at ?? null;
};
// Only a page that lists a registry provider pays for the registry round trip.
const loadRegistryLogos = async (
providersResponse: ProvidersApiResponse | undefined,
): Promise<Record<string, string>> =>
providersResponse?.data.some((provider) => provider.attributes.is_dynamic)
? getRegistryProviderLogos().catch(() => ({}))
: {};
const enrichProviders = (
providersResponse: ProvidersApiResponse | undefined,
schedulesByProviderId: Record<string, ScheduleAttributes>,
@@ -557,6 +566,16 @@ export async function loadProvidersAccountsViewData({
data: [],
};
const providersRequest = resolveActionResult(
getProviders({
filters: providerFilters,
page,
pageSize,
query,
sort: encodedSort,
}),
);
const [
providersResponse,
allProvidersResponse,
@@ -565,15 +584,7 @@ export async function loadProvidersAccountsViewData({
organizationsResponse,
organizationNodesResponse,
] = await Promise.all([
resolveActionResult(
getProviders({
filters: providerFilters,
page,
pageSize,
query,
sort: encodedSort,
}),
),
providersRequest,
// Unfiltered fetch for ProviderTypeSelector — only needs distinct types;
// TODO: Replace with a dedicated lightweight endpoint when available.
resolveActionResult(getAllProviders()),
@@ -619,6 +630,7 @@ export async function loadProvidersAccountsViewData({
providerGroups: allProviderGroupsResponse?.data ?? [],
rows,
hierarchyStatus,
registryLogos: providersRequest.then(loadRegistryLogos),
};
}
@@ -50,6 +50,7 @@ export const ProvidersTabContent = async ({
metadata={providersView.metadata}
rows={providersView.rows}
hierarchyStatus={providersView.hierarchyStatus}
registryLogos={providersView.registryLogos}
scanConfigs={scanConfigsState.data}
scanConfigStatus={scanConfigsState.status}
/>
@@ -0,0 +1 @@
Kubernetes kubeconfig form validation accepts only inline credentials and known cluster fields, rejecting file-path, command-based, `auth-provider`, proxy URL and unknown fields before submission
@@ -0,0 +1 @@
`next` to 16.3.8, patching a server-side request forgery in Image Optimization (GHSA-cjq9-62q9-8jv4)
@@ -0,0 +1 @@
`proxy-addr` to 2.0.8, patching IP spoofing through IPv4-mapped IPv6 trust subnets (GHSA-jqcg-44mw-7w3h)
@@ -0,0 +1 @@
`sharp` to 0.35.5, patching a librsvg memory vulnerability that can lead to remote code execution on glibc-based Linux (GHSA-wq5f-xc86-pv6w)
@@ -10,6 +10,7 @@ import { MutedFindingsConfigButton } from "@/components/providers/muted-findings
import { NoProvidersAdded } from "@/components/providers/no-providers-added";
import { ProvidersAccountsTable } from "@/components/providers/providers-accounts-table";
import { ProvidersFilters } from "@/components/providers/providers-filters";
import { RegistryLogosContext } from "@/components/providers/table/registry-row-logo";
import { ProviderWizardModal } from "@/components/providers/wizard";
import type {
OrgWizardInitialData,
@@ -81,6 +82,8 @@ interface ProvidersAccountsViewProps {
scanConfigStatus?: ScanConfigurationListStatus;
isScanLimitReached?: boolean;
hierarchyStatus?: HierarchyStatus;
/** Registry logo per provider type; rows render before it resolves. */
registryLogos?: Promise<Record<string, string>>;
}
export function ProvidersAccountsView({
@@ -95,6 +98,7 @@ export function ProvidersAccountsView({
scanConfigStatus,
isScanLimitReached,
hierarchyStatus = HIERARCHY_STATUS.AVAILABLE,
registryLogos,
}: ProvidersAccountsViewProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -219,18 +223,20 @@ export function ProvidersAccountsView({
</>
}
/>
<ProvidersAccountsTable
isCloud={isCloud}
metadata={metadata}
rows={rows}
scanScheduleCapability={scanScheduleCapability}
scanConfigs={scanConfigs}
scanConfigStatus={scanConfigStatus}
onOpenProviderWizard={(initialData) =>
openProviderWizard(WIZARD_OPEN_SOURCE.ROW_ACTION, initialData)
}
onOpenOrganizationWizard={openOrganizationWizard}
/>
<RegistryLogosContext value={registryLogos ?? null}>
<ProvidersAccountsTable
isCloud={isCloud}
metadata={metadata}
rows={rows}
scanScheduleCapability={scanScheduleCapability}
scanConfigs={scanConfigs}
scanConfigStatus={scanConfigStatus}
onOpenProviderWizard={(initialData) =>
openProviderWizard(WIZARD_OPEN_SOURCE.ROW_ACTION, initialData)
}
onOpenOrganizationWizard={openOrganizationWizard}
/>
</RegistryLogosContext>
</div>
)}
<ProviderWizardModal
@@ -0,0 +1,59 @@
import { useForm } from "react-hook-form";
import { describe, expect, it } from "vitest";
import { handleRegistryLogo } from "@/__tests__/msw/handlers/registry";
import { worker } from "@/__tests__/msw/worker";
import { render } from "@/__tests__/render-browser";
import type { AddProviderFormValues } from "@/types/formSchemas";
import { RadioGroupProvider } from "./radio-group-provider";
const LOGO_URL = "https://media.registry.example.com/providers/vcf/logo.png";
function Selector() {
const form = useForm<AddProviderFormValues>();
return (
<RadioGroupProvider
control={form.control}
isInvalid={false}
registryAvailable
registryOptions={[
{
type: "vcf",
label: "Vcf",
logoUrl: LOGO_URL,
},
]}
/>
);
}
// The logo is the first element of the option's label row.
function logoBox(option: HTMLElement) {
const logo = option.lastElementChild?.firstElementChild;
if (!logo) throw new Error("Provider option without a logo");
const { width, height } = logo.getBoundingClientRect();
return { width, height };
}
describe("provider selector logos", () => {
it("draws a Registry provider's logo at the size of the built-in logos", async () => {
// Given
worker.use(handleRegistryLogo(LOGO_URL));
const screen = await render(<Selector />);
// When
const builtIn = screen
.getByRole("option", { name: "Amazon Web Services" })
.element() as HTMLElement;
const registry = screen
.getByRole("option", { name: "Vcf Registry" })
.element() as HTMLElement;
await expect
.poll(() => registry.querySelector("img")?.getAttribute("src"))
.toBe(LOGO_URL);
// Then
expect(logoBox(registry)).toEqual(logoBox(builtIn));
});
});
@@ -8,11 +8,6 @@ import {
PROVIDER_TYPE_DATA,
} from "@/components/icons/providers-badge/provider-type-icon";
import { Badge, SearchInput } from "@/components/shadcn";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/shadcn/avatar";
import { FormMessage } from "@/components/shadcn/form";
import {
Tabs,
@@ -24,6 +19,8 @@ import type { RegistryProviderOption } from "@/lib/registry/provider-options";
import { cn } from "@/lib/utils";
import type { AddProviderFormValues } from "@/types/formSchemas";
import { RegistryProviderLogo } from "./registry-provider-logo";
const PROVIDERS = Object.entries(PROVIDER_TYPE_DATA).map(
([value, { label }]) => ({ value, label }),
);
@@ -135,19 +132,11 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
<div className="flex min-w-0 flex-1 items-center gap-1.5">
{provider.registry ? (
<Avatar>
<AvatarImage
src={
provider.logoUrl?.startsWith("https://")
? provider.logoUrl
: undefined
}
alt=""
/>
<AvatarFallback>
<ProviderTypeIcon type={provider.value} size={26} />
</AvatarFallback>
</Avatar>
<RegistryProviderLogo
type={provider.value}
logoUrl={provider.logoUrl}
size={26}
/>
) : (
<ProviderTypeIcon type={provider.value} size={26} />
)}
@@ -0,0 +1,33 @@
import { ProviderTypeIcon } from "@/components/icons/providers-badge/provider-type-icon";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/shadcn/avatar";
import type { ProviderType } from "@/types";
interface RegistryProviderLogoProps {
type: ProviderType;
logoUrl?: string;
size: number;
}
/** The registry's logo, or the generic glyph while it loads or once its signed URL expires. */
export function RegistryProviderLogo({
type,
logoUrl,
size,
}: RegistryProviderLogoProps) {
// Same box as a built-in provider badge of this size.
return (
<Avatar style={{ width: size, height: size }}>
<AvatarImage
src={logoUrl?.startsWith("https://") ? logoUrl : undefined}
alt=""
/>
<AvatarFallback>
<ProviderTypeIcon type={type} size={size} />
</AvatarFallback>
</Avatar>
);
}
@@ -41,6 +41,7 @@ import type {
import { LinkToScans } from "../link-to-scans";
import { DataTableRowActions } from "./data-table-row-actions";
import { RegistryRowLogo } from "./registry-row-logo";
interface GroupNameChipsProps {
groupNames?: string[];
@@ -247,6 +248,11 @@ export function getColumnProviders(
>
<EntityInfo
cloudProvider={provider.attributes.provider}
icon={
provider.attributes.is_dynamic ? (
<RegistryRowLogo type={provider.attributes.provider} />
) : undefined
}
entityAlias={provider.attributes.alias}
entityId={provider.attributes.uid}
nameAction={
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { handleRegistryLogo } from "@/__tests__/msw/handlers/registry";
import { worker } from "@/__tests__/msw/worker";
import { render } from "@/__tests__/render-browser";
import { RegistryLogosContext, RegistryRowLogo } from "./registry-row-logo";
const LOGO_URL = "https://media.registry.example.com/providers/vcf/logo.png";
describe("registry row logo", () => {
it("shows the generic glyph until the streamed logos arrive, then the registry logo", async () => {
// Given
worker.use(handleRegistryLogo(LOGO_URL));
let resolveLogos!: (logos: Record<string, string>) => void;
const logos = new Promise<Record<string, string>>((resolve) => {
resolveLogos = resolve;
});
const screen = await render(
<RegistryLogosContext value={logos}>
<RegistryRowLogo type="vcf" />
</RegistryLogosContext>,
);
const logoImage = () => screen.container.querySelector("img");
// Then: the row renders while the registry has not answered.
expect(screen.container.querySelector("svg")).not.toBeNull();
expect(logoImage()).toBeNull();
// When
resolveLogos({ vcf: LOGO_URL });
// Then
await expect.poll(() => logoImage()?.getAttribute("src")).toBe(LOGO_URL);
});
});
@@ -0,0 +1,39 @@
"use client";
import { createContext, Suspense, use } from "react";
import { RegistryProviderLogo } from "@/components/providers/registry-provider-logo";
import type { ProviderType } from "@/types";
const ROW_LOGO_SIZE = 35;
/** Registry logo per provider type, resolved after the rows render. */
export const RegistryLogosContext = createContext<Promise<
Record<string, string>
> | null>(null);
function ResolvedRegistryRowLogo({
type,
logos,
}: {
type: ProviderType;
logos: Promise<Record<string, string>>;
}) {
const byType = use(logos);
const logoUrl = Object.hasOwn(byType, type) ? byType[type] : undefined;
return (
<RegistryProviderLogo type={type} logoUrl={logoUrl} size={ROW_LOGO_SIZE} />
);
}
/** A registry row's logo; the generic glyph stands in until the logos arrive. */
export function RegistryRowLogo({ type }: { type: ProviderType }) {
const logos = use(RegistryLogosContext);
const placeholder = <RegistryProviderLogo type={type} size={ROW_LOGO_SIZE} />;
if (!logos) return placeholder;
return (
<Suspense fallback={placeholder}>
<ResolvedRegistryRowLogo type={type} logos={logos} />
</Suspense>
);
}
@@ -10,6 +10,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import openaiSchema from "@/lib/provider-credentials/fixtures/openai-credential-schema.json";
import templateSchema from "@/lib/provider-credentials/fixtures/template-credential-schema.json";
import unionSchema from "@/lib/provider-credentials/fixtures/union-credential-schema.json";
import { useProviderWizardStore } from "@/store/provider-wizard/store";
import type { ProviderSchemasResult } from "@/types/provider-schema";
@@ -156,12 +157,16 @@ describe("dynamic credentials in the provider wizard", () => {
expect(timeout).toHaveAttribute("min", "1");
expect(timeout).toHaveAttribute("max", "300");
expect(timeout).toHaveAttribute("step", "1");
expect(
screen.getByRole("combobox", { name: "Authentication Scheme" }),
).toHaveTextContent("bearer");
const authScheme = screen.getByRole("combobox", {
name: "Authentication Scheme",
});
expect(authScheme).toHaveTextContent("Bearer");
expect(apiUrl).toHaveValue("");
// When: false must remain a boolean and numeric input must become a number.
// When: options read capitalized but submit their raw schema value.
await user.click(authScheme);
await user.click(screen.getByRole("option", { name: "Basic" }));
// False must remain a boolean and numeric input must become a number.
await user.type(apiUrl, "https://api.example.test");
await user.type(apiKey, "fixture-key-not-a-secret");
await user.click(verifyTls);
@@ -179,7 +184,78 @@ describe("dynamic credentials in the provider wizard", () => {
api_key: "fixture-key-not-a-secret",
verify_tls: false,
timeout_seconds: 60,
auth_scheme: "bearer",
auth_scheme: "basic",
},
});
});
it("shows only the chosen variant's fields when a schema offers several", async () => {
// Given
const user = userEvent.setup();
getProviderSchemas.mockResolvedValue({
status: "success",
providerType: "acme",
secretTypes: { static: unionSchema },
});
render(<DynamicCredentialsStep {...props} />);
await screen.findByLabelText(/API Host/);
const method = screen.getByRole("combobox", {
name: "Authentication method",
});
// Then
expect(method).toHaveTextContent("API token");
expect(screen.getByLabelText(/^Token/)).toBeVisible();
expect(screen.queryByLabelText(/Username/)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Password/)).not.toBeInTheDocument();
expect(
screen.queryByLabelText(/Authentication Method/),
).not.toBeInTheDocument();
// When: a token typed before switching away is gone on return.
await user.type(screen.getByLabelText(/^Token/), "previous-variant-token");
await user.click(method);
await user.click(
screen.getByRole("option", { name: "Username and password" }),
);
await user.click(method);
await user.click(screen.getByRole("option", { name: "API token" }));
// Then
expect(screen.getByLabelText(/^Token/)).toHaveValue("");
// When
await user.click(method);
await user.click(
screen.getByRole("option", { name: "Username and password" }),
);
await user.type(screen.getByLabelText(/API Host/), "api.acme.test");
await user.type(screen.getByLabelText(/Username/), "fixture-user");
await user.type(
screen.getByLabelText(/Password/),
"fixture-password-not-a-secret",
);
// Then
expect(screen.queryByLabelText(/^Token/)).not.toBeInTheDocument();
// When
act(() =>
screen
.getByLabelText(/Username/)
.closest("form")!
.requestSubmit(),
);
// Then: the variant's fixed discriminator travels with its own fields only.
await waitFor(() => expect(props.onNext).toHaveBeenCalledOnce());
expect(saveDynamicProviderCredentials).toHaveBeenCalledWith({
providerId: "account",
secretType: "static",
secret: {
auth_method: "basic",
host: "api.acme.test",
username: "fixture-user",
password: "fixture-password-not-a-secret",
},
});
});
@@ -275,7 +351,7 @@ describe("dynamic credentials in the provider wizard", () => {
await user.click(
screen.getByRole("combobox", { name: "Authentication method" }),
);
await user.click(screen.getByRole("option", { name: "personal token" }));
await user.click(screen.getByRole("option", { name: "Personal token" }));
expect(screen.getByLabelText(/API token/)).toHaveValue("");
expect(JSON.stringify(sessionStorage)).not.toContain(
"previous-method-secret",
@@ -20,15 +20,19 @@ import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
import { useToast } from "@/components/shadcn/toast";
import { StatusAlert } from "@/components/shared/status-alert";
import {
parseRegistryCredentialSchema,
parseRegistryCredentialVariants,
type RegistryCredentialSchema,
} from "@/lib/provider-credentials/provider-credential-schema";
import {
getCredentialDefaults,
validateCredentialValues,
} from "@/lib/provider-credentials/provider-credential-values";
import { formatRegistryLabel } from "@/lib/registry/labels";
import { useProviderWizardStore } from "@/store/provider-wizard/store";
import type { ProviderSchemasResult } from "@/types/provider-schema";
import type {
ProviderSchemasResult,
ProviderSecretTypes,
} from "@/types/provider-schema";
import {
WIZARD_FOOTER_ACTION_TYPE,
@@ -43,6 +47,39 @@ interface DynamicCredentialsStepProps {
onFooterChange: (config: WizardFooterConfig) => void;
}
interface CredentialMethod {
id: string;
secretType: string;
label: string;
schema: RegistryCredentialSchema | null;
}
// Each variant of a secret type's schema is a method of its own.
function listCredentialMethods(
secretTypes: ProviderSecretTypes,
): CredentialMethod[] {
return Object.entries(secretTypes).flatMap(([secretType, value]) => {
const label = formatRegistryLabel(secretType);
const variants = parseRegistryCredentialVariants(value);
if (!variants || variants.length === 1) {
return [
{
id: secretType,
secretType,
label,
schema: variants?.[0].schema ?? null,
},
];
}
return variants.map((variant, index) => ({
id: `${secretType}/${index}`,
secretType,
label: variant.label ?? `${label} ${index + 1}`,
schema: variant.schema,
}));
});
}
function credentialFormError(status: ProviderSchemasResult["status"]) {
switch (status) {
case "access_denied":
@@ -233,12 +270,11 @@ function DynamicCredentialsContent(props: DynamicCredentialsStepProps) {
}, [providerType, attempt]);
const methods =
schemas?.status === "success" ? Object.keys(schemas.secretTypes) : [];
const secretType = selectedMethod || methods[0];
const schema =
schemas?.status === "success" && secretType
? parseRegistryCredentialSchema(schemas.secretTypes[secretType])
: null;
schemas?.status === "success"
? listCredentialMethods(schemas.secretTypes)
: [];
const method = methods.find(({ id }) => id === selectedMethod) ?? methods[0];
const schema = method?.schema ?? null;
useEffect(() => {
if (!schema)
onFooterChange({
@@ -277,7 +313,7 @@ function DynamicCredentialsContent(props: DynamicCredentialsStepProps) {
Authentication method
</FieldLabel>
<Select
value={secretType}
value={method?.id}
disabled={saving}
onValueChange={setSelectedMethod}
>
@@ -285,20 +321,20 @@ function DynamicCredentialsContent(props: DynamicCredentialsStepProps) {
<SelectValue />
</SelectTrigger>
<SelectContent>
{methods.map((method) => (
<SelectItem key={method} value={method}>
{method.replaceAll("_", " ")}
{methods.map(({ id, label }) => (
<SelectItem key={id} value={id}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)}
{schema ? (
{method && schema ? (
<DynamicCredentialForm
key={`${secretType}/${attempt}`}
key={`${method.id}/${attempt}`}
{...props}
secretType={secretType}
secretType={method.secretType}
schema={schema}
onLoadingChange={setSaving}
/>
@@ -282,13 +282,15 @@ describe("InviteTeammatePanel", () => {
// When
const { onUiState } = renderPanel();
// Then
// Then: the footer drops the action a commit after the message shows.
expect(
await screen.findByText(/Roles could not be loaded right now/),
).toBeInTheDocument();
await waitFor(() =>
expect(
screen.queryByRole("button", { name: "Send invitation" }),
).not.toBeInTheDocument(),
);
expect(lastUiState(onUiState)).toMatchObject({ showAction: false });
expect(
screen.queryByRole("button", { name: "Send invitation" }),
).not.toBeInTheDocument();
});
});
@@ -5,8 +5,8 @@ import { Control, useWatch } from "react-hook-form";
import { WizardTextareaField } from "@/components/providers/workflow/forms/fields";
import { KubernetesCredentials } from "@/types";
import {
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
kubeconfigContainsUnsupportedCommandAuthentication,
KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
kubeconfigIsInlineOnlyCredentials,
} from "@/types/formSchemas";
export const KubernetesCredentialsForm = ({
@@ -18,8 +18,9 @@ export const KubernetesCredentialsForm = ({
control,
name: "kubeconfig_content",
});
const hasUnsupportedCommandAuthentication =
kubeconfigContainsUnsupportedCommandAuthentication(kubeconfigContent ?? "");
const hasUnsupportedCredentials = !kubeconfigIsInlineOnlyCredentials(
kubeconfigContent ?? "",
);
return (
<>
@@ -41,9 +42,9 @@ export const KubernetesCredentialsForm = ({
minRows={10}
isRequired
/>
{hasUnsupportedCommandAuthentication && (
{hasUnsupportedCredentials && (
<p className="text-text-error-primary text-xs">
{KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR}
{KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR}
</p>
)}
</>
@@ -17,6 +17,7 @@ import type {
RegistryCredentialSchema,
RegistryCredentialValue,
} from "@/lib/provider-credentials/provider-credential-schema";
import { formatRegistryLabel } from "@/lib/registry/labels";
interface RegistryCredentialFieldsProps {
readonly errors: Readonly<Record<string, string | undefined>>;
@@ -38,6 +39,7 @@ export function RegistryCredentialFields({
return (
<div className="flex flex-col gap-4">
{schema.fields.map((field, index) => {
if (field.kind === "constant") return null;
const error = errors[field.name];
const fieldId = `registry-credential-${instanceId}-${index}`;
const id = `${fieldId}-control`;
@@ -106,7 +108,7 @@ export function RegistryCredentialFields({
<SelectContent>
{field.options?.map((option) => (
<SelectItem key={option} value={option}>
{option}
{formatRegistryLabel(option)}
</SelectItem>
))}
</SelectContent>
+6 -6
View File
@@ -498,10 +498,10 @@
{
"section": "dependencies",
"name": "next",
"from": "16.3.3",
"to": "16.3.6",
"from": "16.3.6",
"to": "16.3.8",
"strategy": "installed",
"generatedAt": "2026-10-01T09:28:54.112Z"
"generatedAt": "2026-10-08T12:43:06.476Z"
},
{
"section": "dependencies",
@@ -594,10 +594,10 @@
{
"section": "dependencies",
"name": "sharp",
"from": "0.35.3",
"to": "0.35.4",
"from": "0.35.4",
"to": "0.35.5",
"strategy": "installed",
"generatedAt": "2026-09-09T12:39:08.649Z"
"generatedAt": "2026-10-07T09:34:53.599Z"
},
{
"section": "dependencies",
@@ -0,0 +1,71 @@
{
"description": "Acme API credentials.",
"discriminator": {
"mapping": {
"basic": "#/$defs/AcmeBasicCredentials",
"token": "#/$defs/AcmeTokenCredentials"
},
"propertyName": "auth_method"
},
"oneOf": [
{
"additionalProperties": false,
"description": "A long-lived API token issued from the console.",
"properties": {
"auth_method": {
"const": "token",
"default": "token",
"title": "Authentication Method",
"type": "string"
},
"host": {
"description": "Hostname of the API.",
"examples": ["api.acme.test"],
"title": "API Host",
"type": "string"
},
"token": {
"description": "API token.",
"format": "password",
"title": "Token",
"type": "string",
"writeOnly": true
}
},
"required": ["host", "token"],
"title": "API token",
"type": "object"
},
{
"additionalProperties": false,
"description": "A username and password exchanged for a session token.",
"properties": {
"auth_method": {
"const": "basic",
"title": "Authentication Method",
"type": "string"
},
"host": {
"description": "Hostname of the API.",
"examples": ["api.acme.test"],
"title": "API Host",
"type": "string"
},
"username": {
"title": "Username",
"type": "string"
},
"password": {
"format": "password",
"title": "Password",
"type": "string",
"writeOnly": true
}
},
"required": ["auth_method", "host", "username", "password"],
"title": "Username and password",
"type": "object"
}
],
"title": "AcmeStaticCredentials"
}
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import openaiSchema from "./fixtures/openai-credential-schema.json";
import templateSchema from "./fixtures/template-credential-schema.json";
import unionSchema from "./fixtures/union-credential-schema.json";
import {
parseRegistryCredentialSchema,
parseRegistryCredentialVariants,
REGISTRY_CREDENTIAL_SCHEMA_LIMITS,
} from "./provider-credential-schema";
@@ -157,10 +159,22 @@ describe("parseRegistryCredentialSchema", () => {
["definitions", { definitions: {} }],
["combinators", { anyOf: [] }],
["additional properties", { additionalProperties: true }],
["typed additional properties", { additionalProperties: {} }],
])("rejects risky root keywords: %s", (_name, keyword) => {
expect(parseRegistryCredentialSchema({ ...schema, ...keyword })).toBeNull();
});
it("accepts the closed object pydantic emits for extra='forbid'", () => {
// Given / When
const result = parseRegistryCredentialSchema({
...schema,
additionalProperties: false,
});
// Then
expect(result).toEqual(parseRegistryCredentialSchema(schema));
});
it.each([
["nested objects", { type: "object", properties: {} }],
["arrays", { type: "array" }],
@@ -264,3 +278,98 @@ describe("parseRegistryCredentialSchema", () => {
]);
});
});
describe("parseRegistryCredentialVariants", () => {
it("splits a pydantic union into variants that keep only their own fields", () => {
// Given / When
const result = parseRegistryCredentialVariants(unionSchema);
// Then
expect(
result?.map(({ label, schema }) => [
label,
schema.fields.map(({ name, kind }) => [name, kind]),
]),
).toEqual([
[
"API token",
[
["auth_method", "constant"],
["host", "text"],
["token", "password"],
],
],
[
"Username and password",
[
["auth_method", "constant"],
["host", "text"],
["username", "text"],
["password", "password"],
],
],
]);
expect(result?.[1].schema.fields[0].defaultValue).toBe("basic");
});
it("accepts anyOf, which pydantic emits for a union without discriminator", () => {
// Given
const { oneOf, discriminator: _discriminator, ...root } = unionSchema;
// When
const result = parseRegistryCredentialVariants({ ...root, anyOf: oneOf });
// Then
expect(result?.map(({ label }) => label)).toEqual([
"API token",
"Username and password",
]);
});
it.each([
["an unsupported variant", { oneOf: [templateSchema, { type: "array" }] }],
["no variants", { oneOf: [] }],
["variants that are not a list", { oneOf: templateSchema }],
["both combinators", { oneOf: [templateSchema], anyOf: [templateSchema] }],
["unknown root keywords", { oneOf: [templateSchema], $defs: {} }],
[
"a malformed discriminator",
{ oneOf: [templateSchema], discriminator: 1 },
],
[
"too many variants",
{
oneOf: Array.from(
{ length: REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_VARIANTS + 1 },
() => templateSchema,
),
},
],
])("rejects a union with %s", (_name, union) => {
expect(parseRegistryCredentialVariants(union)).toBeNull();
});
it.each([
{ type: "string", const: "token", default: "basic" },
{ type: "string", const: "" },
{ type: "string", const: "token", enum: ["token"] },
{ type: "integer", const: 1 },
])("rejects a discriminator that is not one fixed string: %j", (property) => {
expect(
parseRegistryCredentialSchema({
type: "object",
properties: { auth_method: property },
}),
).toBeNull();
});
it("returns a flat schema as its only variant", () => {
// Given / When
const result = parseRegistryCredentialVariants(templateSchema);
// Then
expect(result).toEqual([
{ schema: parseRegistryCredentialSchema(templateSchema) },
]);
});
});
@@ -5,6 +5,7 @@ const FIELD_KIND = {
TEXTAREA: "textarea",
CHECKBOX: "checkbox",
INTEGER: "integer",
CONSTANT: "constant",
} as const;
export const REGISTRY_CREDENTIAL_SCHEMA_LIMITS = {
@@ -12,6 +13,7 @@ export const REGISTRY_CREDENTIAL_SCHEMA_LIMITS = {
MAX_NAME_LENGTH: 50,
MAX_TEXT_LENGTH: 200,
MAX_ENUM_OPTIONS: 20,
MAX_VARIANTS: 8,
} as const;
type FieldKind = (typeof FIELD_KIND)[keyof typeof FIELD_KIND];
@@ -34,7 +36,18 @@ export interface RegistryCredentialSchema {
readonly fields: readonly RegistryCredentialField[];
}
const ROOT = new Set("type title description properties required".split(" "));
/** One alternative of a `oneOf`/`anyOf` schema, or the whole flat schema. */
export interface RegistryCredentialVariant {
readonly label?: string;
readonly schema: RegistryCredentialSchema;
}
const ROOT = new Set(
"type title description properties required additionalProperties".split(" "),
);
const UNION_ROOT = new Set(
"title description oneOf anyOf discriminator".split(" "),
);
const FIELD = new Set(
"title description type format writeOnly enum default examples x-prowler-widget".split(
" ",
@@ -44,6 +57,9 @@ const BOOLEAN_FIELD = new Set("title description type default".split(" "));
const INTEGER_FIELD = new Set(
"title description type default minimum maximum".split(" "),
);
const CONSTANT_FIELD = new Set(
"title description type const default".split(" "),
);
const FORBIDDEN_NAMES = new Set(["__proto__", "prototype", "constructor"]);
const FIELD_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
@@ -87,7 +103,14 @@ function hasOnly(
export function parseRegistryCredentialSchema(
value: unknown,
): RegistryCredentialSchema | null {
if (!isRecord(value) || !hasOnly(value, ROOT) || value.type !== "object") {
if (
!isRecord(value) ||
!hasOnly(value, ROOT) ||
value.type !== "object" ||
// Only a closed object: the form never sends undeclared keys anyway.
(value.additionalProperties !== undefined &&
value.additionalProperties !== false)
) {
return null;
}
if (
@@ -189,6 +212,22 @@ export function parseRegistryCredentialSchema(
});
continue;
}
if (property.type === "string" && property.const !== undefined) {
// A union's discriminator: fixed by the variant, never typed by the user.
if (
!hasOnly(property, CONSTANT_FIELD) ||
!isText(property.const) ||
(defaultValue !== undefined && defaultValue !== property.const)
) {
return null;
}
fields.push({
...baseField,
kind: FIELD_KIND.CONSTANT,
defaultValue: property.const,
});
continue;
}
if (property.type !== "string" || !hasOnly(property, FIELD)) return null;
const format = property.format;
@@ -256,3 +295,42 @@ export function parseRegistryCredentialSchema(
}
return { fields };
}
export function parseRegistryCredentialVariants(
value: unknown,
): readonly RegistryCredentialVariant[] | null {
const isUnion =
isRecord(value) &&
(Object.hasOwn(value, "oneOf") || Object.hasOwn(value, "anyOf"));
if (!isUnion) {
const schema = parseRegistryCredentialSchema(value);
return schema ? [{ schema }] : null;
}
if (
!hasOnly(value, UNION_ROOT) ||
(Object.hasOwn(value, "oneOf") && Object.hasOwn(value, "anyOf")) ||
(value.title !== undefined && !isText(value.title)) ||
(value.description !== undefined &&
typeof value.description !== "string") ||
(value.discriminator !== undefined && !isRecord(value.discriminator))
) {
return null;
}
const branches = value.oneOf ?? value.anyOf;
if (
!Array.isArray(branches) ||
branches.length === 0 ||
branches.length > REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_VARIANTS
) {
return null;
}
const variants: RegistryCredentialVariant[] = [];
for (const branch of branches) {
const schema = parseRegistryCredentialSchema(branch);
if (!schema) return null;
const label = isRecord(branch) ? branch.title : undefined;
variants.push({ ...(isText(label) ? { label } : {}), schema });
}
return variants;
}
@@ -1,6 +1,7 @@
import type {
RegistryCredentialSchema,
RegistryCredentialValue,
RegistryCredentialVariant,
} from "./provider-credential-schema";
export function getCredentialDefaults(
@@ -17,16 +18,18 @@ export function getCredentialDefaults(
);
}
export function validateCredentialValues(
schema: RegistryCredentialSchema,
values: unknown,
):
type CredentialValidation =
| {
valid: true;
secret: Record<string, RegistryCredentialValue>;
errors: Record<string, string>;
}
| { valid: false; errors: Record<string, string> } {
| { valid: false; errors: Record<string, string> };
export function validateCredentialValues(
schema: RegistryCredentialSchema,
values: unknown,
): CredentialValidation {
if (!values || typeof values !== "object" || Array.isArray(values))
return {
valid: false,
@@ -47,7 +50,17 @@ export function validateCredentialValues(
const secret: Record<string, RegistryCredentialValue> = {};
for (const field of schema.fields) {
const value = fields.get(field.name);
if (value === undefined || (value === "" && field.kind !== "checkbox")) {
if (field.kind === "constant") {
// Always sent: it tells the API which variant the secret follows.
if (value !== undefined && value !== field.defaultValue) {
errors[field.name] = `Enter a valid ${field.label}`;
} else if (field.defaultValue !== undefined) {
secret[field.name] = field.defaultValue;
}
} else if (
value === undefined ||
(value === "" && field.kind !== "checkbox")
) {
if (field.required) errors[field.name] = `${field.label} is required`;
} else if (field.kind === "checkbox") {
if (typeof value !== "boolean") {
@@ -83,3 +96,19 @@ export function validateCredentialValues(
? { valid: false, errors }
: { valid: true, secret, errors };
}
/** Valid when one variant accepts the values, as JSON Schema `anyOf` does. */
export function validateCredentialVariants(
variants: readonly RegistryCredentialVariant[],
values: unknown,
): CredentialValidation {
const results = variants.map(({ schema }) =>
validateCredentialValues(schema, values),
);
// Otherwise report the variant that owns every submitted field.
return (
results.find(({ valid }) => valid) ??
results.find(({ errors }) => !errors._form) ??
results[0]
);
}
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { formatRegistryLabel } from "./labels";
describe("formatRegistryLabel", () => {
it("capitalizes plain identifiers and keeps any other value as written", () => {
// When
const labels = ["token", "service_account", "us-east-1", "OAuth2"].map(
formatRegistryLabel,
);
// Then
expect(labels).toEqual(["Token", "Service account", "us-east-1", "OAuth2"]);
});
});
+9
View File
@@ -0,0 +1,9 @@
// Only plain lowercase identifiers: "us-east-1" or "OAuth2" already read as meant.
const PLAIN_IDENTIFIER = /^[a-z][a-z0-9]*(?:[_ ][a-z0-9]+)*$/;
/** Display text for a Registry identifier ("service_account" → "Service account"). */
export function formatRegistryLabel(value: string): string {
if (!PLAIN_IDENTIFIER.test(value)) return value;
const words = value.replaceAll("_", " ");
return words.charAt(0).toUpperCase() + words.slice(1);
}
+21
View File
@@ -143,4 +143,25 @@ describe("installed Registry provider options", () => {
).map((option) => option.type),
).toEqual(["zeta"]);
});
it("capitalizes provider names the registry publishes as plain slugs", () => {
// Given: the registry names one provider by its slug and another properly.
const catalog = [
{ ...provider, normalizedName: "vcf-package", providerSlug: "vcf" },
{ ...provider, normalizedName: "openai-package", providerSlug: "openai" },
];
const installed = catalog.map(({ normalizedName }) => ({
normalizedName,
versionSpec: "latest",
}));
// When
const labels = buildRegistryProviderOptions(catalog, installed, [
{ type: "vcf", label: "vcf" },
{ type: "openai", label: "OpenAI" },
]).map((option) => option.label);
// Then
expect(labels).toEqual(["OpenAI", "Vcf"]);
});
});
+5 -3
View File
@@ -1,3 +1,4 @@
import { formatRegistryLabel } from "@/lib/registry/labels";
import { isKnownProviderType } from "@/types/providers";
import type {
RegistryCatalogArtifact,
@@ -63,7 +64,8 @@ export function buildRegistryProviderOptions(
);
}
}
return Array.from(options.values()).sort((left, right) =>
left.label.localeCompare(right.label),
);
return Array.from(options.values(), (option) => ({
...option,
label: formatRegistryLabel(option.label),
})).sort((left, right) => left.label.localeCompare(right.label));
}
+2 -2
View File
@@ -98,7 +98,7 @@
"marked": "15.0.12",
"modern-screenshot": "4.7.0",
"nanoid": "5.1.16",
"next": "16.3.6",
"next": "16.3.8",
"next-auth": "5.0.0-beta.32",
"next-themes": "0.2.1",
"posthog-js": "1.407.2",
@@ -110,7 +110,7 @@
"recharts": "2.15.4",
"require-in-the-middle": "8.0.1",
"server-only": "0.0.1",
"sharp": "0.35.4",
"sharp": "0.35.5",
"streamdown": "1.6.10",
"tailwind-merge": "3.3.1",
"tailwindcss-animate": "1.0.7",
+180 -179
View File
@@ -12,7 +12,7 @@ overrides:
'@react-aria/visually-hidden>react': 19.2.7
'@react-aria/interactions>react': 19.2.7
lodash: 4.18.1
sharp: 0.35.4
sharp: 0.35.5
lodash-es: 4.18.1
hono: 4.12.34
'@hono/node-server': 1.19.17
@@ -44,6 +44,7 @@ overrides:
ip-address: 10.3.1
mermaid: 11.16.1
body-parser: 2.3.0
proxy-addr: 2.0.8
'@humanfs/node': 0.16.8
js-yaml: 4.3.1
@@ -92,7 +93,7 @@ importers:
version: 1.2.3
'@next/third-parties':
specifier: 16.2.9
version: 16.2.9(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
version: 16.2.9(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
'@radix-ui/react-alert-dialog':
specifier: 1.1.14
version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -167,7 +168,7 @@ importers:
version: 3.26.0(react@19.2.7)
'@sentry/nextjs':
specifier: 10.65.0
version: 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))
version: 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))
'@tailwindcss/postcss':
specifier: 4.1.18
version: 4.1.18
@@ -238,14 +239,14 @@ importers:
specifier: 5.1.16
version: 5.1.16
next:
specifier: 16.3.6
version: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
specifier: 16.3.8
version: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next-auth:
specifier: 5.0.0-beta.32
version: 5.0.0-beta.32(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
version: 5.0.0-beta.32(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
next-themes:
specifier: 0.2.1
version: 0.2.1(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
version: 0.2.1(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
posthog-js:
specifier: 1.407.2
version: 1.407.2(preact-render-to-string@6.5.11(preact@10.24.3))
@@ -274,8 +275,8 @@ importers:
specifier: 0.0.1
version: 0.0.1
sharp:
specifier: 0.35.4
version: 0.35.4(@types/node@24.10.8)
specifier: 0.35.5
version: 0.35.5(@types/node@24.10.8)
streamdown:
specifier: 1.6.10
version: 1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7)
@@ -1101,160 +1102,160 @@ packages:
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.4':
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
'@img/sharp-darwin-arm64@0.35.5':
resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.4':
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
'@img/sharp-darwin-x64@0.35.5':
resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.4':
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
'@img/sharp-freebsd-wasm32@0.35.5':
resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.3':
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
'@img/sharp-libvips-darwin-arm64@1.3.4':
resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.3':
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
'@img/sharp-libvips-darwin-x64@1.3.4':
resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.3':
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
'@img/sharp-libvips-linux-arm64@1.3.4':
resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.3':
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
'@img/sharp-libvips-linux-arm@1.3.4':
resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.3':
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
'@img/sharp-libvips-linux-ppc64@1.3.4':
resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.3':
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
'@img/sharp-libvips-linux-riscv64@1.3.4':
resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.3':
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
'@img/sharp-libvips-linux-s390x@1.3.4':
resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.3':
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
'@img/sharp-libvips-linux-x64@1.3.4':
resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.4':
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
'@img/sharp-linux-arm64@0.35.5':
resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.4':
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
'@img/sharp-linux-arm@0.35.5':
resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.4':
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
'@img/sharp-linux-ppc64@0.35.5':
resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.4':
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
'@img/sharp-linux-riscv64@0.35.5':
resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.4':
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
'@img/sharp-linux-s390x@0.35.5':
resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.4':
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
'@img/sharp-linux-x64@0.35.5':
resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.4':
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
'@img/sharp-linuxmusl-arm64@0.35.5':
resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.4':
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
'@img/sharp-linuxmusl-x64@0.35.5':
resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.4':
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
'@img/sharp-wasm32@0.35.5':
resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.4':
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
'@img/sharp-webcontainers-wasm32@0.35.5':
resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.4':
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
'@img/sharp-win32-arm64@0.35.5':
resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.4':
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
'@img/sharp-win32-ia32@0.35.5':
resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.4':
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
'@img/sharp-win32-x64@0.35.5':
resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
@@ -1414,60 +1415,60 @@ packages:
'@ndaidong/bellajs@12.0.1':
resolution: {integrity: sha512-1iY42uiHz0cxNMbde7O3zVN+ZX1viOOUOBRt6ht6lkRZbSjwOnFV34Zv4URp3hGzEe6L9Byk7BOq/41H0PzAOQ==}
'@next/env@16.3.6':
resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==}
'@next/env@16.3.8':
resolution: {integrity: sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==}
'@next/eslint-plugin-next@16.2.9':
resolution: {integrity: sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==}
'@next/swc-darwin-arm64@16.3.6':
resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==}
'@next/swc-darwin-arm64@16.3.8':
resolution: {integrity: sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.3.6':
resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==}
'@next/swc-darwin-x64@16.3.8':
resolution: {integrity: sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.3.6':
resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==}
'@next/swc-linux-arm64-gnu@16.3.8':
resolution: {integrity: sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.3.6':
resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==}
'@next/swc-linux-arm64-musl@16.3.8':
resolution: {integrity: sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.3.6':
resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==}
'@next/swc-linux-x64-gnu@16.3.8':
resolution: {integrity: sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.3.6':
resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==}
'@next/swc-linux-x64-musl@16.3.8':
resolution: {integrity: sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.3.6':
resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==}
'@next/swc-win32-arm64-msvc@16.3.8':
resolution: {integrity: sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.3.6':
resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==}
'@next/swc-win32-x64-msvc@16.3.8':
resolution: {integrity: sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -5801,8 +5802,8 @@ packages:
react: '*'
react-dom: '*'
next@16.3.6:
resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==}
next@16.3.8:
resolution: {integrity: sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -6164,8 +6165,8 @@ packages:
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
proxy-addr@2.0.8:
resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
proxy-from-env@1.1.0:
@@ -6484,8 +6485,8 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.35.4:
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
sharp@0.35.5:
resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
@@ -8310,108 +8311,108 @@ snapshots:
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.4':
'@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.3
'@img/sharp-libvips-darwin-arm64': 1.3.4
optional: true
'@img/sharp-darwin-x64@0.35.4':
'@img/sharp-darwin-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.3
'@img/sharp-libvips-darwin-x64': 1.3.4
optional: true
'@img/sharp-freebsd-wasm32@0.35.4':
'@img/sharp-freebsd-wasm32@0.35.5':
dependencies:
'@img/sharp-wasm32': 0.35.4
'@img/sharp-wasm32': 0.35.5
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.3':
'@img/sharp-libvips-darwin-arm64@1.3.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.3':
'@img/sharp-libvips-darwin-x64@1.3.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.3':
'@img/sharp-libvips-linux-arm64@1.3.4':
optional: true
'@img/sharp-libvips-linux-arm@1.3.3':
'@img/sharp-libvips-linux-arm@1.3.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.3':
'@img/sharp-libvips-linux-ppc64@1.3.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.3':
'@img/sharp-libvips-linux-riscv64@1.3.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.3':
'@img/sharp-libvips-linux-s390x@1.3.4':
optional: true
'@img/sharp-libvips-linux-x64@1.3.3':
'@img/sharp-libvips-linux-x64@1.3.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
optional: true
'@img/sharp-linux-arm64@0.35.4':
'@img/sharp-linux-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.3
'@img/sharp-libvips-linux-arm64': 1.3.4
optional: true
'@img/sharp-linux-arm@0.35.4':
'@img/sharp-linux-arm@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.3
'@img/sharp-libvips-linux-arm': 1.3.4
optional: true
'@img/sharp-linux-ppc64@0.35.4':
'@img/sharp-linux-ppc64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.3
'@img/sharp-libvips-linux-ppc64': 1.3.4
optional: true
'@img/sharp-linux-riscv64@0.35.4':
'@img/sharp-linux-riscv64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.3
'@img/sharp-libvips-linux-riscv64': 1.3.4
optional: true
'@img/sharp-linux-s390x@0.35.4':
'@img/sharp-linux-s390x@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.3
'@img/sharp-libvips-linux-s390x': 1.3.4
optional: true
'@img/sharp-linux-x64@0.35.4':
'@img/sharp-linux-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.3
'@img/sharp-libvips-linux-x64': 1.3.4
optional: true
'@img/sharp-linuxmusl-arm64@0.35.4':
'@img/sharp-linuxmusl-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
optional: true
'@img/sharp-linuxmusl-x64@0.35.4':
'@img/sharp-linuxmusl-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
optional: true
'@img/sharp-wasm32@0.35.4':
'@img/sharp-wasm32@0.35.5':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.4':
'@img/sharp-webcontainers-wasm32@0.35.5':
dependencies:
'@img/sharp-wasm32': 0.35.4
'@img/sharp-wasm32': 0.35.5
optional: true
'@img/sharp-win32-arm64@0.35.4':
'@img/sharp-win32-arm64@0.35.5':
optional: true
'@img/sharp-win32-ia32@0.35.4':
'@img/sharp-win32-ia32@0.35.5':
optional: true
'@img/sharp-win32-x64@0.35.4':
'@img/sharp-win32-x64@0.35.5':
optional: true
'@inquirer/ansi@2.0.5': {}
@@ -8630,39 +8631,39 @@ snapshots:
'@ndaidong/bellajs@12.0.1': {}
'@next/env@16.3.6': {}
'@next/env@16.3.8': {}
'@next/eslint-plugin-next@16.2.9':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@16.3.6':
'@next/swc-darwin-arm64@16.3.8':
optional: true
'@next/swc-darwin-x64@16.3.6':
'@next/swc-darwin-x64@16.3.8':
optional: true
'@next/swc-linux-arm64-gnu@16.3.6':
'@next/swc-linux-arm64-gnu@16.3.8':
optional: true
'@next/swc-linux-arm64-musl@16.3.6':
'@next/swc-linux-arm64-musl@16.3.8':
optional: true
'@next/swc-linux-x64-gnu@16.3.6':
'@next/swc-linux-x64-gnu@16.3.8':
optional: true
'@next/swc-linux-x64-musl@16.3.6':
'@next/swc-linux-x64-musl@16.3.8':
optional: true
'@next/swc-win32-arm64-msvc@16.3.6':
'@next/swc-win32-arm64-msvc@16.3.8':
optional: true
'@next/swc-win32-x64-msvc@16.3.6':
'@next/swc-win32-x64-msvc@16.3.8':
optional: true
'@next/third-parties@16.2.9(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)':
'@next/third-parties@16.2.9(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)':
dependencies:
next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
third-party-capital: 1.0.20
@@ -9829,7 +9830,7 @@ snapshots:
dependencies:
'@sentry/core': 10.65.0
'@sentry/nextjs@10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))':
'@sentry/nextjs@10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))':
dependencies:
'@opentelemetry/api': 1.9.1
'@rollup/plugin-commonjs': 28.0.1(rollup@4.59.0)
@@ -9842,7 +9843,7 @@ snapshots:
'@sentry/react': 10.65.0(react@19.2.7)
'@sentry/vercel-edge': 10.65.0
'@sentry/webpack-plugin': 5.4.0(rollup@4.59.0)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))
next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
rollup: 4.59.0
stacktrace-parser: 0.1.11
transitivePeerDependencies:
@@ -12002,7 +12003,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.2.1
router: 2.2.0
@@ -13437,21 +13438,21 @@ snapshots:
neo-async@2.6.2: {}
next-auth@5.0.0-beta.32(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
next-auth@5.0.0-beta.32(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
dependencies:
'@auth/core': 0.41.3
next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
next-themes@0.2.1(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
next-themes@0.2.1(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@next/env': 16.3.6
'@next/env': 16.3.8
'@swc/helpers': 0.5.23
baseline-browser-mapping: 2.10.29
caniuse-lite: 1.0.30001792
@@ -13460,18 +13461,18 @@ snapshots:
react-dom: 19.2.7(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
optionalDependencies:
'@next/swc-darwin-arm64': 16.3.6
'@next/swc-darwin-x64': 16.3.6
'@next/swc-linux-arm64-gnu': 16.3.6
'@next/swc-linux-arm64-musl': 16.3.6
'@next/swc-linux-x64-gnu': 16.3.6
'@next/swc-linux-x64-musl': 16.3.6
'@next/swc-win32-arm64-msvc': 16.3.6
'@next/swc-win32-x64-msvc': 16.3.6
'@next/swc-darwin-arm64': 16.3.8
'@next/swc-darwin-x64': 16.3.8
'@next/swc-linux-arm64-gnu': 16.3.8
'@next/swc-linux-arm64-musl': 16.3.8
'@next/swc-linux-x64-gnu': 16.3.8
'@next/swc-linux-x64-musl': 16.3.8
'@next/swc-win32-arm64-msvc': 16.3.8
'@next/swc-win32-x64-msvc': 16.3.8
'@opentelemetry/api': 1.9.1
'@playwright/test': 1.56.1
babel-plugin-react-compiler: 1.0.0
sharp: 0.35.4(@types/node@24.10.8)
sharp: 0.35.5(@types/node@24.10.8)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
@@ -13794,7 +13795,7 @@ snapshots:
property-information@7.1.0: {}
proxy-addr@2.0.7:
proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -14228,37 +14229,37 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.35.4(@types/node@24.10.8):
sharp@0.35.5(@types/node@24.10.8):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.4
'@img/sharp-darwin-x64': 0.35.4
'@img/sharp-freebsd-wasm32': 0.35.4
'@img/sharp-libvips-darwin-arm64': 1.3.3
'@img/sharp-libvips-darwin-x64': 1.3.3
'@img/sharp-libvips-linux-arm': 1.3.3
'@img/sharp-libvips-linux-arm64': 1.3.3
'@img/sharp-libvips-linux-ppc64': 1.3.3
'@img/sharp-libvips-linux-riscv64': 1.3.3
'@img/sharp-libvips-linux-s390x': 1.3.3
'@img/sharp-libvips-linux-x64': 1.3.3
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
'@img/sharp-linux-arm': 0.35.4
'@img/sharp-linux-arm64': 0.35.4
'@img/sharp-linux-ppc64': 0.35.4
'@img/sharp-linux-riscv64': 0.35.4
'@img/sharp-linux-s390x': 0.35.4
'@img/sharp-linux-x64': 0.35.4
'@img/sharp-linuxmusl-arm64': 0.35.4
'@img/sharp-linuxmusl-x64': 0.35.4
'@img/sharp-webcontainers-wasm32': 0.35.4
'@img/sharp-win32-arm64': 0.35.4
'@img/sharp-win32-ia32': 0.35.4
'@img/sharp-win32-x64': 0.35.4
'@img/sharp-darwin-arm64': 0.35.5
'@img/sharp-darwin-x64': 0.35.5
'@img/sharp-freebsd-wasm32': 0.35.5
'@img/sharp-libvips-darwin-arm64': 1.3.4
'@img/sharp-libvips-darwin-x64': 1.3.4
'@img/sharp-libvips-linux-arm': 1.3.4
'@img/sharp-libvips-linux-arm64': 1.3.4
'@img/sharp-libvips-linux-ppc64': 1.3.4
'@img/sharp-libvips-linux-riscv64': 1.3.4
'@img/sharp-libvips-linux-s390x': 1.3.4
'@img/sharp-libvips-linux-x64': 1.3.4
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
'@img/sharp-linux-arm': 0.35.5
'@img/sharp-linux-arm64': 0.35.5
'@img/sharp-linux-ppc64': 0.35.5
'@img/sharp-linux-riscv64': 0.35.5
'@img/sharp-linux-s390x': 0.35.5
'@img/sharp-linux-x64': 0.35.5
'@img/sharp-linuxmusl-arm64': 0.35.5
'@img/sharp-linuxmusl-x64': 0.35.5
'@img/sharp-webcontainers-wasm32': 0.35.5
'@img/sharp-win32-arm64': 0.35.5
'@img/sharp-win32-ia32': 0.35.5
'@img/sharp-win32-x64': 0.35.5
'@types/node': 24.10.8
shebang-command@2.0.0:
+10 -4
View File
@@ -18,10 +18,11 @@ overrides:
"@react-aria/visually-hidden>react": "19.2.7"
"@react-aria/interactions>react": "19.2.7"
"lodash": "4.18.1"
# Next.js 16.3.6 requests sharp ^0.35.4; pinned to fix
# GHSA-rgj7-g3m4-5g8c (libheif). This override controls resolution;
# keep it aligned with the direct dependency in package.json.
"sharp": "0.35.4"
# Next.js 16.3.8 requests sharp ^0.35.4; pinned to fix
# GHSA-rgj7-g3m4-5g8c (libheif) and GHSA-wq5f-xc86-pv6w (librsvg).
# This override controls resolution; keep it aligned with the direct
# dependency in package.json.
"sharp": "0.35.5"
"lodash-es": "4.18.1"
# GHSA-88fw-hqm2-52qc (CORS reflects any Origin with credentials), CVE-2026-59896
# (hono/jsx SSR context leak) and the 4.12.34 batch: CORS ReDoS via
@@ -107,6 +108,11 @@ overrides:
"mermaid": "11.16.1"
# body-parser (via express): invalid `limit` silently disabled size enforcement.
"body-parser": "2.3.0"
# proxy-addr (via @modelcontextprotocol/sdk > express): GHSA-jqcg-44mw-7w3h
# (critical), a short IPv4-mapped IPv6 trust subnet trusts every client, so
# `req.ip` returns a spoofed X-Forwarded-For. 2.0.8 (2026-09-15) clears the
# 7-day cooldown gate.
"proxy-addr": "2.0.8"
# @humanfs/node (via eslint): recursive copy followed symlinks outside the tree.
"@humanfs/node": "0.16.8"
# js-yaml: quadratic CPU in `!!omap` resolution (CVE-2026-59870 not backported
+2
View File
@@ -23,6 +23,8 @@ const EXPECTED_CONTROL_FLOW_MESSAGES = [
"NEXT_NOT_FOUND",
"AbortError",
"ResizeObserver",
// Client disconnected mid-stream; Next.js forwards it to onRequestError (vercel/next.js#96704)
"The destination stream closed early",
] as const;
const HTTP_CONTEXT_MESSAGES = [
+113 -9
View File
@@ -7,7 +7,7 @@ import {
addCredentialsFormSchema,
addCredentialsRoleFormSchema,
addProviderFormSchema,
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
roleFormSchema,
samlConfigFormSchema,
} from "./formSchemas";
@@ -222,7 +222,7 @@ users:
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
});
@@ -249,17 +249,42 @@ users:
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
});
it("accepts kubeconfig auth-provider without cmd-path", () => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1
it.each([
[
"user tokenFile",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
tokenFile: /etc/passwd`,
],
[
"user client-certificate",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
client-certificate: /etc/ssl/cert.pem`,
],
[
"user client-key",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
client-key: /etc/ssl/key.pem`,
],
[
"user auth-provider directives",
`apiVersion: v1
kind: Config
users:
- name: test-user
@@ -268,6 +293,85 @@ users:
name: oidc
config:
client-id: prowler`,
],
[
"unknown user key",
`apiVersion: v1
kind: Config
users:
- name: test-user
user:
bogus: value`,
],
[
"cluster certificate-authority",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
certificate-authority: /etc/ssl/ca.pem`,
],
[
"cluster proxy-url",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
proxy-url: http://proxy.evil.test`,
],
[
"unknown cluster key",
`apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
bogus: /etc/ssl/ca.pem`,
],
])(
"rejects kubeconfig with %s on kubeconfig_content field",
(_label, kubeconfigContent) => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: kubeconfigContent,
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.KUBECONFIG_CONTENT],
message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
}),
);
},
);
it("accepts kubeconfig with only inline credentials", () => {
const schema = addCredentialsFormSchema("kubernetes");
const result = schema.safeParse({
...BASE_KUBERNETES_VALUES,
[ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://example.test
certificate-authority-data: Zm9v
users:
- name: test-user
user:
client-certificate-data: Zm9v
client-key-data: YmFy`,
});
expect(result.success).toBe(true);
+58 -29
View File
@@ -8,42 +8,75 @@ import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS } from "@/types/saml";
import { isKnownProviderType, PROVIDER_TYPES, ProviderType } from "./providers";
export const KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR =
"Kubernetes kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.";
export const KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR =
"Only inline Kubernetes credentials are supported. For user authentication use token, username/password, or client-certificate-data with client-key-data; clusters may use certificate-authority-data. File-path fields (tokenFile, client-certificate, client-key, certificate-authority), command-based authentication (exec, cmd-path), auth-provider directives, and proxy URLs are not supported.";
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
export const kubeconfigContainsUnsupportedCommandAuthentication = (
value: string,
): boolean => {
const KUBECONFIG_ALLOWED_USER_KEYS = new Set([
"token",
"username",
"password",
"client-certificate-data",
"client-key-data",
"as",
"as-uid",
"as-groups",
"as-user-extra",
]);
const KUBECONFIG_ALLOWED_CLUSTER_KEYS = new Set([
"server",
"certificate-authority-data",
"insecure-skip-tls-verify",
"tls-server-name",
"disable-compression",
"extensions",
]);
// Mirrors the API key checks; structural validation stays in the API.
export const kubeconfigIsInlineOnlyCredentials = (value: string): boolean => {
let parsed: unknown;
try {
const parsed = yaml.load(value);
parsed = yaml.load(value);
} catch {
return true;
}
if (!isRecord(parsed) || !Array.isArray(parsed.users)) {
return false;
}
if (!isRecord(parsed)) {
return true;
}
return parsed.users.some((userEntry) => {
if (Array.isArray(parsed.users)) {
const usersInlineOnly = parsed.users.every((userEntry) => {
if (!isRecord(userEntry) || !isRecord(userEntry.user)) {
return false;
}
if ("exec" in userEntry.user) {
return true;
}
const authProvider = userEntry.user["auth-provider"];
if (!isRecord(authProvider) || !isRecord(authProvider.config)) {
return false;
}
return "cmd-path" in authProvider.config;
return Object.keys(userEntry.user).every((key) =>
KUBECONFIG_ALLOWED_USER_KEYS.has(key),
);
});
} catch {
return false;
if (!usersInlineOnly) {
return false;
}
}
if (Array.isArray(parsed.clusters)) {
const clustersInlineOnly = parsed.clusters.every((clusterEntry) => {
if (!isRecord(clusterEntry) || !isRecord(clusterEntry.cluster)) {
return true;
}
return Object.keys(clusterEntry.cluster).every((key) =>
KUBECONFIG_ALLOWED_CLUSTER_KEYS.has(key),
);
});
if (!clustersInlineOnly) {
return false;
}
}
return true;
};
// Create and edit share the same shape, so a single schema backs both flows.
@@ -263,13 +296,9 @@ export const addCredentialsFormSchema = (
.string()
.min(1, "Kubeconfig Content is required")
.refine(
(value) =>
!kubeconfigContainsUnsupportedCommandAuthentication(
value,
),
(value) => kubeconfigIsInlineOnlyCredentials(value),
{
error:
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
error: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR,
},
),
}
+2
View File
@@ -105,6 +105,8 @@ export interface ProvidersAccountsViewData {
rows: ProvidersTableRow[];
/** `unavailable` when the hierarchy fetch failed (drives the degraded notice). */
hierarchyStatus: HierarchyStatus;
/** Registry logo per provider type, streamed so rows never wait for it. */
registryLogos: Promise<Record<string, string>>;
}
export function isProvidersOrganizationRow(
Generated
+81 -88
View File
@@ -219,7 +219,7 @@ constraints = [
{ name = "typing-inspection", specifier = "==0.4.2" },
{ name = "tzdata", specifier = "==2026.2" },
{ name = "uritemplate", specifier = "==4.2.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "urllib3", specifier = "==2.8.0" },
{ name = "vulture", specifier = "==2.14" },
{ name = "websocket-client", specifier = "==1.9.0" },
{ name = "werkzeug", specifier = "==3.1.8" },
@@ -230,7 +230,10 @@ constraints = [
{ name = "zipp", specifier = "==3.23.1" },
{ name = "zstd", specifier = "==1.5.7.2" },
]
overrides = [{ name = "okta", specifier = "==3.4.2" }]
overrides = [
{ name = "okta", specifier = "==3.4.2" },
{ name = "pymongo", specifier = "==4.18.2" },
]
[[package]]
name = "about-time"
@@ -1922,41 +1925,41 @@ wheels = [
[[package]]
name = "dulwich"
version = "1.2.5"
version = "1.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/85/ceb8ecff5cdeee4ceeebb86b599476dee559041dacc6c2c50cc0d4711549/dulwich-1.2.5.tar.gz", hash = "sha256:0395b2c8924c3424bafe2d9c1edd5348cc4b21ce9c1d6655bf01f9a5c47164c8", size = 1253230, upload-time = "2026-05-28T22:27:55.17Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/17/0a0fa2b998cd9a852b1aa901e3f6145f13a4334f64b91564847e5880968d/dulwich-1.2.9.tar.gz", hash = "sha256:b395466b78eb5c36baf34381b8b831fd26da8a44b428a2eea1d57ba53f68a234", size = 1309797, upload-time = "2026-07-06T20:57:23.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/af/60929b502d6541cb015beb9f1da82600aac64d5f705d0188aaf44a7aa77f/dulwich-1.2.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:46db47394ba8a95748ae739f5d3a5a3e1724a2f857bf2437bc71bfc0baaed91d", size = 1400236, upload-time = "2026-05-28T22:26:50.998Z" },
{ url = "https://files.pythonhosted.org/packages/04/f8/25de359a9249cc05a58c2500babfe2adff174931f2fa3fe97c700ca16626/dulwich-1.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66aded7d364341b55941973a1562323f25bd205f0809692b687ec36ccd31242c", size = 1382996, upload-time = "2026-05-28T22:26:53.545Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/640fee144007262096173f5fafd04cc7e5a0d72b0ceeeb9c9a51d99abc43/dulwich-1.2.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:dd9569bc26174a3437d749114d36c81fc6c7478b55370ae50125e34e9629e4fe", size = 1471811, upload-time = "2026-05-28T22:26:55.044Z" },
{ url = "https://files.pythonhosted.org/packages/51/2a/348c1f0baa0c42dc79a9f503463ddb00452c234ec5c9e20b43530d78528e/dulwich-1.2.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:827366331603150de5976d72dd456a3fd5fc91e856471dc1d10fd64758c05f02", size = 1498006, upload-time = "2026-05-28T22:26:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/d4/f8/95f51bcbc2ce86beea49a67f61228be86bf614a24aa714a8c59e0abdd153/dulwich-1.2.5-cp310-cp310-win32.whl", hash = "sha256:6c683c0f4a062894b6826c61102d415dae86ade61a10003c82ccc2b91858d5fb", size = 1066964, upload-time = "2026-05-28T22:26:58.049Z" },
{ url = "https://files.pythonhosted.org/packages/68/c1/ffa02a1623c3d668de8e66b654187bb8dc24c085224644e5554537ee4642/dulwich-1.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:a6620963196c49212c511cd909f367dacf771f199a27d116f357cc671ea956c7", size = 1079537, upload-time = "2026-05-28T22:26:59.319Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4a/654ae1671610fdf6b65a64586ad67ddd8550d4d08a632b2a4b9614754b6d/dulwich-1.2.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:556593fd11637f80f6018bee1916b1a84f5b420423b470ebb3f1a782ad6ef081", size = 1399277, upload-time = "2026-05-28T22:27:00.801Z" },
{ url = "https://files.pythonhosted.org/packages/85/d8/06ee3bc8eded4bd7adf8adf0c9ea5f19bf96f7e5e626bfaf7311cde4208a/dulwich-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a70477c991e96cfe8fdd7c866e7251faf71b38bfeb51d6f27554c9cce1caabf3", size = 1382310, upload-time = "2026-05-28T22:27:02.216Z" },
{ url = "https://files.pythonhosted.org/packages/07/17/a03adf50b9095f9f5d863393f21d585dea39bdc4fdf60788ff3a9407a512/dulwich-1.2.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9008ef25cabd379cda4fa86000fc38ca14b72afe17db798a8c85c0b2b7ce4d1e", size = 1470993, upload-time = "2026-05-28T22:27:04.075Z" },
{ url = "https://files.pythonhosted.org/packages/60/58/1dc352d2a5e80befe4338af7208febb44bcfd7496b0dde5ac6dacb07b031/dulwich-1.2.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a5549f4afc973e0a15ea6b0244d57f848d3f3ee13dac557eb311024aebebf128", size = 1497820, upload-time = "2026-05-28T22:27:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a8/e058959a87e7df7753b112ef66a43ccbc57338c1bbdc23a0edf3833396df/dulwich-1.2.5-cp311-cp311-win32.whl", hash = "sha256:5108acead814d1de8b6262d6d8fb90af7e82f5a4d83788b6b48e39d01800a92f", size = 1066549, upload-time = "2026-05-28T22:27:06.832Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/ff0b444f686718635348986bd73dfce42e947912417893de35de399b878b/dulwich-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:5e067b7feceb7034bc99e7c7143a704f1d97d4be7027d9a0aa5a83c0657ff091", size = 1079481, upload-time = "2026-05-28T22:27:08.33Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/4f75770bbe5521cac61c4820ef46d4fbf8c2175d3519ba3d0378d4ba798e/dulwich-1.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:701a9ecf7a8a44f5e2459e46befa93530cf36a8b1ae3140aefc007db1d7d0207", size = 1396522, upload-time = "2026-05-28T22:27:09.997Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b1/c07c347681c0cf6acd4b189bf6e8d6207c71a1347b7a1e865eb40faa46b9/dulwich-1.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f90d68bfa97c4ca71de7507984365aefe27b6d248cb28dc99644d0f3ae8c60b", size = 1334826, upload-time = "2026-05-28T22:27:11.582Z" },
{ url = "https://files.pythonhosted.org/packages/13/80/6818eb7ce492e18ab2efa92ab901d173b4b0b159e5681c1424f329600c40/dulwich-1.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00b54a1d56ddbacdd8eadd6d4787a51b3a05fefa30eadbf9165fd283a00b90ed", size = 1416616, upload-time = "2026-05-28T22:27:13.195Z" },
{ url = "https://files.pythonhosted.org/packages/14/a7/9790e60d19870f6554f7583722bb324c1355784316f20aeda1c0b5b1491a/dulwich-1.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8f7ea8f47e38e5b0de3fab97e07e9c9161ffddc90b3964512cab2b7749df4e6", size = 1441354, upload-time = "2026-05-28T22:27:14.683Z" },
{ url = "https://files.pythonhosted.org/packages/91/44/0ea8a69c24aa1254ff5996d682eae2eab287d471b937dcdb26d9ea9720b4/dulwich-1.2.5-cp312-cp312-win32.whl", hash = "sha256:8929134acf4ff967203df7600b38535f9b5b590462067a7e30dbce01acb97af9", size = 1017058, upload-time = "2026-05-28T22:27:16.121Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/2fcddda7faec3bae52db7c64bfcb5dc756f597f33fae90e8d4e4b4d3b39b/dulwich-1.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:9693d2c9e226b2ea855c1dc3a87e2f4d972f7523fc0f7924e5997e9f4c23d97f", size = 1031731, upload-time = "2026-05-28T22:27:17.633Z" },
{ url = "https://files.pythonhosted.org/packages/f5/33/8bd311edfb312e907b4ec7ad4680922e3f5baeccd18d987aeb57141a685d/dulwich-1.2.5-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2f10dafa1ef5660b1331364bc8d68446448608a8d8f493ed0e260eaf5133e71c", size = 1530974, upload-time = "2026-05-28T22:27:19.301Z" },
{ url = "https://files.pythonhosted.org/packages/f5/0d/e4704188a78efbe785d6dc921b70a4f896dbbd4e37caed3a4122be31e871/dulwich-1.2.5-cp313-cp313-android_21_x86_64.whl", hash = "sha256:9b6d234f1f91335e9f01d9daac42ddc2d2e5c2fdbe285d8eeef50353b283648a", size = 1524254, upload-time = "2026-05-28T22:27:20.993Z" },
{ url = "https://files.pythonhosted.org/packages/62/95/feb632693a629314593972f9393bb0a614aee5301ebd0848cb1c7fbfa56c/dulwich-1.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c65230abaa52c72093b70d3b499d5689d1d8f9627e88ad3c3b4f8154e86ac0c8", size = 1394846, upload-time = "2026-05-28T22:27:22.475Z" },
{ url = "https://files.pythonhosted.org/packages/1b/4c/f0b4ae4cd2a34bf21ac37944d75c68d2297ebc0128928f988767d1542ef0/dulwich-1.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e128cddeccae4146b556684a0d5426454fff5bfe7306862e5a8ce6b471568af4", size = 1333523, upload-time = "2026-05-28T22:27:23.966Z" },
{ url = "https://files.pythonhosted.org/packages/0f/bf/b834306cd6a5959b1521d7f4d3e7473d39cdaea43063d97fa1eed6f1ba28/dulwich-1.2.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:07cb75b58216440e2c170fff4f3d55a5f387358d9489863af8cb11f24ee37121", size = 1415849, upload-time = "2026-05-28T22:27:25.329Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/8a6110eb9314fea1bf79c13ab1a5ab699167f1401284cee4bebbf3db2e90/dulwich-1.2.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:df4ac3746099562c8160d78d55bb2fa10c9ada7ef970af3e2536bd133cb7830e", size = 1441075, upload-time = "2026-05-28T22:27:26.723Z" },
{ url = "https://files.pythonhosted.org/packages/00/75/e993287749f7fa9ff871a0a4fbbede81727f982b1658ba50e7273a100fd3/dulwich-1.2.5-cp313-cp313-win32.whl", hash = "sha256:9fc113c1348c7eb22c4e8790f68b562bb4f42a721fafb813e89a57e9cd632040", size = 1016835, upload-time = "2026-05-28T22:27:28.544Z" },
{ url = "https://files.pythonhosted.org/packages/50/bc/17cc36a67696e06e040e8035fcfdc2583e3834f736b7fe808d3ce01531d0/dulwich-1.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:1ba83ec3cfb4c506c277400357a51523c8258fa07b841ee06e8e1071da4cfed1", size = 1031444, upload-time = "2026-05-28T22:27:30.204Z" },
{ url = "https://files.pythonhosted.org/packages/07/4b/4a18a59ad230581cd0ef460e96001f90762e566dc2dfdba22aa358eb5a0e/dulwich-1.2.5-py3-none-any.whl", hash = "sha256:1679b376433a0fc7f36586afda1d4ed7427afa7a79d4bf17e5014474eea69fa4", size = 686745, upload-time = "2026-05-28T22:27:53.695Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d8/5144092504b20df4e4baf1006f099f7f53ca57353ccbf5e24f3da13db25d/dulwich-1.2.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:90c79e0fe54f63f6b56aacc9b5fdfd20771810607a1dc18fa2a78754bf6abade", size = 1379387, upload-time = "2026-07-06T20:56:10.005Z" },
{ url = "https://files.pythonhosted.org/packages/14/02/cc814bbae0c9763317fff593597b6d860275777f72f54d791c8d99c18257/dulwich-1.2.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9dcae6b21ca4ddafe30f79caf3fdb3137214886cc09e6bf1e67e9f5bb61e8206", size = 1373369, upload-time = "2026-07-06T20:56:11.854Z" },
{ url = "https://files.pythonhosted.org/packages/61/a8/6db716095113107b2c72b9a8a95480c71206d87ee994345c06ffefffe14a/dulwich-1.2.9-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2c8760ad8cd09d26cc2da891bcb65e3ffc3b4dc4ca58d4377d9cd0712cb9dd37", size = 1452849, upload-time = "2026-07-06T20:56:13.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/b1/a9c720ede886341f8b037951a5b9dac4387f8bceaad7f76d15e9e28f1b30/dulwich-1.2.9-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a94c107eb1b8e397dd6440248f939e31756c4738eaa038d3416b52bad8ece94c", size = 1476940, upload-time = "2026-07-06T20:56:15.134Z" },
{ url = "https://files.pythonhosted.org/packages/7f/c8/6738a67a5cb112880834b8d785c24555295dfa06d8fa5ab29d7bf9c12b98/dulwich-1.2.9-cp310-cp310-win32.whl", hash = "sha256:27dc86b999f25f314205e723a51860851563b7ec144ed852acdaf9c2e6c4a714", size = 1043723, upload-time = "2026-07-06T20:56:16.673Z" },
{ url = "https://files.pythonhosted.org/packages/d3/40/3d6e4960daae19145f7e90778b017280e4b8683071077f68ee8bbe54ec2d/dulwich-1.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:5a2923f2cc4abc4ced2769a54cdec51be949717208ab9a31fe2565fe27f09bbb", size = 1061250, upload-time = "2026-07-06T20:56:18.576Z" },
{ url = "https://files.pythonhosted.org/packages/95/70/0345eb2b07a9b26337d7e9ee2c0fc634be81de9969d683b72eca04878f72/dulwich-1.2.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b01fd4d8bd4fdd2bd99eb465e101b4fac2b50436e317b971da7ac3a1bd3a83d8", size = 1378231, upload-time = "2026-07-06T20:56:20.301Z" },
{ url = "https://files.pythonhosted.org/packages/56/dd/a699facf7371cb26233ea1f659ff172995cc06a999ba07992d2762a7dc1f/dulwich-1.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e92f76f0048b076ef692094d28645c5e0f65dae1d5e393fbb2e8db2edc017cd3", size = 1371812, upload-time = "2026-07-06T20:56:21.958Z" },
{ url = "https://files.pythonhosted.org/packages/35/02/73baf09f61b66431adb1cd01a9fbe748369a62814b6e27362daaaa924f04/dulwich-1.2.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ac47e02988bdcde0c23f98275908390448617d491ad5d9cb9f33cf3eb8ad512b", size = 1493888, upload-time = "2026-07-06T20:56:23.697Z" },
{ url = "https://files.pythonhosted.org/packages/48/e4/aa2dff296c4a9cd769b5a55a8de68622c1605ad39d75b741527e63117acf/dulwich-1.2.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5e6eb07fbd288059da71019f6746655cd1e136db2f1664fa3e1c81bc11973187", size = 1523051, upload-time = "2026-07-06T20:56:25.525Z" },
{ url = "https://files.pythonhosted.org/packages/24/da/73162905ffb2041a8f484e46c7bb269cb39cc984335b1e66eef23d38cb1d/dulwich-1.2.9-cp311-cp311-win32.whl", hash = "sha256:e96f5d29bf509ceb2dbf9cf4de67fe6e5d785b22f721814360563be6757e7473", size = 1043350, upload-time = "2026-07-06T20:56:27.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/e6/8e40755771b5d05c9268771f26cef8f2ba5f7a849bedcee06504d7f842ae/dulwich-1.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:255c4c3605582fe8958afeff94fc1f0883aedc37495c43a74e031b904f3bb86d", size = 1103738, upload-time = "2026-07-06T20:56:29.145Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b0/e4f14c603fa42e07d8a7f1d5a73f123a8148d52b79b3cc478797126dcdd3/dulwich-1.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d5a2896b6dc3f0c794d34b3eb1af317a43b557fefbee0099ef83d35bb6e102f", size = 1372001, upload-time = "2026-07-06T20:56:30.605Z" },
{ url = "https://files.pythonhosted.org/packages/c9/b7/11c40f1c3ce304e7c2e0e759c589430f043a7967689e61ecb0142ad1ebdc/dulwich-1.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30e72110630e2990d11770227191dfa730b7c045030f818eca5803c6b89bbc5b", size = 1356130, upload-time = "2026-07-06T20:56:32.176Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/10d61b784c295a4567615579272d91ede81793c72ead207484c0ed845ddd/dulwich-1.2.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5c636fcb33c4db5495a2dc7686cf6a26cf537060a859982e104dcf67635ea7f7", size = 1437923, upload-time = "2026-07-06T20:56:33.741Z" },
{ url = "https://files.pythonhosted.org/packages/6e/68/3073e2737e2c3f299e736490ec2e8b5864593858b9d08f36fbcbca80a862/dulwich-1.2.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1aa1b55f59a046fc8f25a67560473c789b7ec113b85c6a08021aae10c6f78945", size = 1463517, upload-time = "2026-07-06T20:56:35.812Z" },
{ url = "https://files.pythonhosted.org/packages/f2/61/f96c0da72b08bfbc4e38571374c3000dee13e6d72dd06f25281d36d5b0b7/dulwich-1.2.9-cp312-cp312-win32.whl", hash = "sha256:e27a06f9f71275d712b616ad23008df5f2dcf54a06cb8e2a24be8819ee1ce13a", size = 1082071, upload-time = "2026-07-06T20:56:38.139Z" },
{ url = "https://files.pythonhosted.org/packages/38/96/567f32885e780aaa7441f9f5c6f162e7271d149e27d3cb661caf939cd671/dulwich-1.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:81e1005ffc5d5c872bfc87ac3442f722bce27ba10e435543d5302853c34abf56", size = 1053612, upload-time = "2026-07-06T20:56:39.977Z" },
{ url = "https://files.pythonhosted.org/packages/55/dc/86419d8e1f740a4dc29e32f529bcc32848bd726e34d71481c8e04764f8cf/dulwich-1.2.9-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:598cfe12262f97e13a7caf7793afd76e909ea9b1f228ff2b0d0af9cd3992829b", size = 1510306, upload-time = "2026-07-06T20:56:41.863Z" },
{ url = "https://files.pythonhosted.org/packages/ea/47/43d1730d42e3f11b9f1561084101e7cdebb40e3a87879f558370647be052/dulwich-1.2.9-cp313-cp313-android_24_x86_64.whl", hash = "sha256:c68e1302c77baa6db49dde60b2878b4f56728316e9f1bd5adc28d8a22058d1ed", size = 1499940, upload-time = "2026-07-06T20:56:43.71Z" },
{ url = "https://files.pythonhosted.org/packages/67/83/1934132defd24dc8cc38c01e5ca5bb271a9f6f8e109b1ca80e6b80e36998/dulwich-1.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d154fc8cb21d393f064057c2965f481464b8d82977c4dff335ce89fb60cd46e7", size = 1370890, upload-time = "2026-07-06T20:56:45.261Z" },
{ url = "https://files.pythonhosted.org/packages/18/1e/da9e332726570f5cdaa9f1227f516fb4eff1b0e4b9121d658239c794d5eb/dulwich-1.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:300573ce5075641d18562da5343f53352c5672465ccd7974ef88d316bf043f84", size = 1355428, upload-time = "2026-07-06T20:56:47.126Z" },
{ url = "https://files.pythonhosted.org/packages/11/d6/375b8e5cab3a001163a93e610c30a3dc4829cf1b17a2afa57ee4caec3f5e/dulwich-1.2.9-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:697fc6dbec9fb33d91a53c36c27bbac7e25e2008fb70e90d6f26e05ac95dfdf2", size = 1437132, upload-time = "2026-07-06T20:56:48.774Z" },
{ url = "https://files.pythonhosted.org/packages/41/ae/c4fd50807495b909e8b7568effd06be2613ab7c6c9d4235756a3037cd817/dulwich-1.2.9-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6d86a1baa6f6dd8a11770cf210cc9193e1debb68a7265dfe6da3682b7167d631", size = 1461912, upload-time = "2026-07-06T20:56:50.487Z" },
{ url = "https://files.pythonhosted.org/packages/17/de/4a64c796afe8862cec6fc800bd01c003e15bac11935cdd2be6565c228faa/dulwich-1.2.9-cp313-cp313-win32.whl", hash = "sha256:6ac4b9dd9d668bf757aae70641adba22f9aa0c916ad45d3f734fdd58edb1add8", size = 1081801, upload-time = "2026-07-06T20:56:52.134Z" },
{ url = "https://files.pythonhosted.org/packages/45/2e/29ab77e0553f94f4ee43b94e41c883f86e092f6fa3edea4838d9dda91071/dulwich-1.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:516f843f9a03c34cf4f5f8b55a459bd8833550f4b44dade1ea7f91ef907788e7", size = 1053482, upload-time = "2026-07-06T20:56:53.963Z" },
{ url = "https://files.pythonhosted.org/packages/cd/f9/a6677c0e698b5b781d663eb394bd15cc69c4a270b11cdcc128076753d75c/dulwich-1.2.9-py3-none-any.whl", hash = "sha256:79ff0ff35c4e17ccd58f9bfb23f4f8d416b0b09337b8c99ee3b4f10459bb0d68", size = 711459, upload-time = "2026-07-06T20:57:21.516Z" },
]
[[package]]
@@ -1986,7 +1989,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -3940,7 +3943,7 @@ requires-dist = [
{ name = "dash", specifier = "==3.1.1" },
{ name = "dash-bootstrap-components", specifier = "==2.0.3" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "dulwich", specifier = "==1.2.5" },
{ name = "dulwich", specifier = "==1.2.9" },
{ name = "google-api-python-client", specifier = "==2.163.0" },
{ name = "google-auth-httplib2", specifier = "==0.2.0" },
{ name = "h2", specifier = "==4.3.0" },
@@ -4332,63 +4335,53 @@ wheels = [
[[package]]
name = "pymongo"
version = "4.15.1"
version = "4.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/19/2de6086e3974f3a95a1fc41fd082bc4a58dc9b70268cbfd7c84067d184f2/pymongo-4.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97ccf8222abd5b79daa29811f64ef8b6bb678b9c9a1c1a2cfa0a277f89facd1d", size = 811020, upload-time = "2025-09-16T16:37:57.329Z" },
{ url = "https://files.pythonhosted.org/packages/a2/a4/a340dde32818dd5c95b1c373bc4a27cef5863009faa328388ddc899527fe/pymongo-4.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f130b3d7540749a8788a254ceb199a03ede4ee080061bfa5e20e28237c87f2d7", size = 811313, upload-time = "2025-09-16T16:37:59.312Z" },
{ url = "https://files.pythonhosted.org/packages/e2/d9/7d64fdc9e87ec38bd36395bc730848ef56e1cd4bd29ab065d53c27559ace/pymongo-4.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fbe6a044a306ed974bd1788f3ceffc2f5e13f81fdb786a28c948c047f4cea38", size = 1188666, upload-time = "2025-09-16T16:38:00.896Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d9/47cc69d3b22c9d971b1486e3a80d6a5d0bbf2dec6c9c4d5e39a129ee8125/pymongo-4.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b96768741e0e03451ef7b07c4857490cc43999e01c7f8da704fe00b3fe5d4d3", size = 1222891, upload-time = "2025-09-16T16:38:02.574Z" },
{ url = "https://files.pythonhosted.org/packages/a9/73/a57594c956bf276069a438056330a346871b2f5e3cae4e3bcc257cffc788/pymongo-4.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50b18ad6e4a55a75c30f0e669bd15ed1ceb18f9994d6835b4f5d5218592b4a0", size = 1205824, upload-time = "2025-09-16T16:38:04.277Z" },
{ url = "https://files.pythonhosted.org/packages/37/d5/1ae77ddcc376ebce0139614d51ec1fd0ba666d7cc1f198ec88272cfdac36/pymongo-4.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e8e2a33613b2880d516d9c8616b64d27957c488de2f8e591945cf12094336a5", size = 1191838, upload-time = "2025-09-16T16:38:05.728Z" },
{ url = "https://files.pythonhosted.org/packages/35/07/ae3fc20a809066b35bbf470bda79d34a72948603d9f29a425bf1d0ef2cb7/pymongo-4.15.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a2a439395f3d4c9d3dc33ba4575d52b6dd285d57db54e32062ae8ef557cab10", size = 1170996, upload-time = "2025-09-16T16:38:09.084Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0f/eb654cea7586588704151ac4894cd3fb2582c0db458cd615cad1c7fe4c59/pymongo-4.15.1-cp310-cp310-win32.whl", hash = "sha256:142abf2fbd4667a3c8f4ce2e30fdbd287c015f52a838f4845d7476a45340208d", size = 798249, upload-time = "2025-09-16T16:38:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/38184382c32695f914a5474d8de0c9f3714b7d8f4c66f090b3836d70273d/pymongo-4.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:8baf46384c97f774bc84178662e1fc6e32a2755fbc8e259f424780c2a11a3566", size = 807990, upload-time = "2025-09-16T16:38:12.525Z" },
{ url = "https://files.pythonhosted.org/packages/38/eb/77a4d37b2a0673c010dd97b9911438f17bb05f407235cc9f02074175855d/pymongo-4.15.1-cp310-cp310-win_arm64.whl", hash = "sha256:b5b837df8e414e2a173722395107da981d178ba7e648f612fa49b7ab4e240852", size = 800875, upload-time = "2025-09-16T16:38:14.532Z" },
{ url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" },
{ url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" },
{ url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" },
{ url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" },
{ url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" },
{ url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" },
{ url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" },
{ url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" },
{ url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" },
{ url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" },
{ url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" },
{ url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" },
{ url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" },
{ url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" },
{ url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" },
{ url = "https://files.pythonhosted.org/packages/a6/22/02ac885d8accb4c86ae92e99681a09f3fd310c431843fc850e141b42ab17/pymongo-4.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:622957eed757e44d9605c43b576ef90affb61176d9e8be7356c1a2948812cb84", size = 974492, upload-time = "2025-09-16T16:38:50.437Z" },
{ url = "https://files.pythonhosted.org/packages/56/bf/71685b6b2d085dbaadf029b1ea4a1bc7a1bc483452513dea283b47a5f7c0/pymongo-4.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c5283dffcf601b793a57bb86819a467473bbb1bf21cd170c0b9648f933f22131", size = 974191, upload-time = "2025-09-16T16:38:52.725Z" },
{ url = "https://files.pythonhosted.org/packages/df/98/141edc92fa97af96b4c691e10a7225ac3e552914e88b7a8d439bd6bc9fcc/pymongo-4.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:def51dea1f8e336aed807eb5d2f2a416c5613e97ec64f07479681d05044c217c", size = 1962311, upload-time = "2025-09-16T16:38:54.319Z" },
{ url = "https://files.pythonhosted.org/packages/f8/a9/601b91607af1dec8035b46ba67a5a023c819ccedd40d6f6232e15bf76030/pymongo-4.15.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24171b2015052b2f0a3f8cbfa38b973fa87f6474e88236a4dfeb735983f9f49e", size = 2039667, upload-time = "2025-09-16T16:38:55.958Z" },
{ url = "https://files.pythonhosted.org/packages/4f/71/02e9a5248e0a9dfc371fd7350f8b11eac03d9eb3662328978f37613d319a/pymongo-4.15.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64b60ed7220c52f8c78c7af8d2c58f7e415732e21b3ff7e642169efa6e0b11e7", size = 2003579, upload-time = "2025-09-16T16:38:57.576Z" },
{ url = "https://files.pythonhosted.org/packages/f9/d1/b1a9520b33e022ed1c0d2d43e8805ba18d3d686fc9c9d89a507593f6dd86/pymongo-4.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58236ce5ba3a79748c1813221b07b411847fd8849ff34c2891ba56f807cce3e5", size = 1964307, upload-time = "2025-09-16T16:38:59.219Z" },
{ url = "https://files.pythonhosted.org/packages/3d/d1/1d205a762020f056c05899a912364c48bac0f3438502b36d057aa1da3ca5/pymongo-4.15.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7461e777b3da96568c1f077b1fbf9e0c15667ac4d8b9a1cf90d80a69fe3be609", size = 1913879, upload-time = "2025-09-16T16:39:01.266Z" },
{ url = "https://files.pythonhosted.org/packages/44/d1/0a3ab2440ea00b6423f33c84e6433022fd51f3561dede9346f54f39cf4dd/pymongo-4.15.1-cp313-cp313-win32.whl", hash = "sha256:45f0a2fb09704ca5e0df08a794076d21cbe5521d3a8ceb8ad6d51cef12f5f4e7", size = 938007, upload-time = "2025-09-16T16:39:03.427Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/e9ea839af2caadfde91774549a6f72450b72efdc92117995e7117d4b1270/pymongo-4.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:b70201a6dbe19d0d10a886989d3ba4b857ea6ef402a22a61c8ca387b937cc065", size = 962236, upload-time = "2025-09-16T16:39:05.018Z" },
{ url = "https://files.pythonhosted.org/packages/ad/f8/0a92a72993b2e1c110ee532650624ca7ae15c5e45906dbae4f063a2fd32a/pymongo-4.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:6892ebf8b2bc345cacfe1301724195d87162f02d01c417175e9f27d276a2f198", size = 944138, upload-time = "2025-09-16T16:39:07.035Z" },
{ url = "https://files.pythonhosted.org/packages/e5/eb/2ba257482844bb2e3c82c6b266d6e811bc610fa80408133e352cc1afb3c9/pymongo-4.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:db439288516514713c8ee09c9baaf66bc4b0188fbe4cd578ef3433ee27699aab", size = 1030987, upload-time = "2025-09-16T16:39:08.914Z" },
{ url = "https://files.pythonhosted.org/packages/0d/86/8c6eab3767251ba77a3604d3b6b0826d0af246bd04b2d16aced3a54f08b0/pymongo-4.15.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:234c80a5f21c8854cc5d6c2f5541ff17dd645b99643587c5e7ed1e21d42003b6", size = 1030996, upload-time = "2025-09-16T16:39:10.429Z" },
{ url = "https://files.pythonhosted.org/packages/5b/26/c1bc0bcb64f39b9891b8b537f21cc37d668edd8b93f47ed930af7f95649c/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b570dc8179dcab980259b885116b14462bcf39170e30d8cbcce6f17f28a2ac5b", size = 2290670, upload-time = "2025-09-16T16:39:12.348Z" },
{ url = "https://files.pythonhosted.org/packages/82/af/f5e8b6c404a3678a99bf0b704f7b19fa14a71edb42d724eb09147aa1d3be/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb6321bde02308d4d313b487d19bfae62ea4d37749fc2325b1c12388e05e4c31", size = 2377711, upload-time = "2025-09-16T16:39:13.992Z" },
{ url = "https://files.pythonhosted.org/packages/af/f4/63bcc1760bf3e0925cb6cb91b2b3ba756c113b1674a14b41efe7e3738b8d/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc808588289f693aba80fae8272af4582a7d6edc4e95fb8fbf65fe6f634116ce", size = 2337097, upload-time = "2025-09-16T16:39:15.717Z" },
{ url = "https://files.pythonhosted.org/packages/d0/dc/0cfada0426556b4b04144fb00ce6a1e7535ab49623d4d9dd052d27ea46c0/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99236fd0e0cf6b048a4370d0df6820963dc94f935ad55a2e29af752272abd6c9", size = 2288295, upload-time = "2025-09-16T16:39:17.385Z" },
{ url = "https://files.pythonhosted.org/packages/5b/a8/081a80f60042d2b8cd6a1c091ecaa186f1ef216b587d06acd0743e1016c6/pymongo-4.15.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2277548bb093424742325b2a88861d913d8990f358fc71fd26004d1b87029bb8", size = 2227616, upload-time = "2025-09-16T16:39:19.025Z" },
{ url = "https://files.pythonhosted.org/packages/56/d0/a6007e0c3c5727391ac5ea40e93a1e7d14146c65ac4ca731c0680962eb48/pymongo-4.15.1-cp313-cp313t-win32.whl", hash = "sha256:754a5d75c33d49691e2b09a4e0dc75959e271a38cbfd92c6b36f7e4eafc4608e", size = 987225, upload-time = "2025-09-16T16:39:20.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/97/c9bf6dcd647a8cf7abbad5814dfb7d8a16e6ab92a3e56343b3bcb454a6d3/pymongo-4.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8d62e68ad21661e536555d0683087a14bf5c74b242a4446c602d16080eb9e293", size = 1017521, upload-time = "2025-09-16T16:39:22.319Z" },
{ url = "https://files.pythonhosted.org/packages/31/ea/102f7c9477302fa05e5303dd504781ac82400e01aab91bfba9c290253bd6/pymongo-4.15.1-cp313-cp313t-win_arm64.whl", hash = "sha256:56bbfb79b51e95f4b1324a5a7665f3629f4d27c18e2002cfaa60c907cc5369d9", size = 992963, upload-time = "2025-09-16T16:39:23.957Z" },
{ url = "https://files.pythonhosted.org/packages/94/19/fe076a243faa8bc8f41d5ecc7c3dd625437bbca6b0dae633e05e8811e6db/pymongo-4.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb4892f6edd939677e767e45087173c7292cfcfa0ea52fb2076e1fb89fbd597f", size = 819557, upload-time = "2026-09-24T16:58:04.8Z" },
{ url = "https://files.pythonhosted.org/packages/23/ad/be444b903d5e712b659ac83b23643e2df2b61631f46d7ece02521caeb7fc/pymongo-4.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f953e62a054030c7c7f923f65da95cb5332230a135ee24e57af2defad7d8cff", size = 820096, upload-time = "2026-09-24T16:58:06.883Z" },
{ url = "https://files.pythonhosted.org/packages/c9/83/9e6b7364e9234689fc17c0897bbac95b6bb032d8d16d6070c4a617c06cf9/pymongo-4.18.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:69de0a25ce088e45c25b4ba3e84ad7f530375b48f6024dda48b1ec34a08b4d4f", size = 1041104, upload-time = "2026-09-24T16:58:08.468Z" },
{ url = "https://files.pythonhosted.org/packages/21/6a/02f792efaed73e56910bbdf3b161c0b85b651807d6a5ab498b60c2637329/pymongo-4.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3fb1f90fcd5aec21535ace955bb72195751c09ea179d751855f7a066a6fb8ab", size = 1049685, upload-time = "2026-09-24T16:58:09.873Z" },
{ url = "https://files.pythonhosted.org/packages/a0/fe/3da97617e4a5c4144429308498507b326332d2271025ca23bbd8fe5b1f99/pymongo-4.18.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2dff5ef3d523caa47ca06e6832578b463e05e2ffb74d637fe633305680ab9d", size = 1074077, upload-time = "2026-09-24T16:58:11.321Z" },
{ url = "https://files.pythonhosted.org/packages/0c/95/1731d74417f1f298f004fe4e9dae19a326e8e743478a4006afab43682ec5/pymongo-4.18.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078b73765963f716e12a7ef9f2247baef22f01c0a7288281cf8fe18ca0f96635", size = 1067691, upload-time = "2026-09-24T16:58:12.747Z" },
{ url = "https://files.pythonhosted.org/packages/7c/6d/cf29ce0b02f38946e8f6e55d232cbb34b6e346c9b3874427bf2c276b7ccf/pymongo-4.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d9922c08c0fc166620b5693829a2852a16082f4fc8fb431e30bf6d0a1f4984", size = 1051651, upload-time = "2026-09-24T16:58:14.26Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ca/425a20beb371f3e7d379425bbbf97496632dbcf0d7b5c4fd2f325435f8b3/pymongo-4.18.2-cp310-cp310-win32.whl", hash = "sha256:899bd9012e72eeec7616456f13f9402f96ed4e0ec110974cf9055082b4c2bb78", size = 814456, upload-time = "2026-09-24T16:58:15.897Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d9/4082e0fe625624bd71d53792cc49b466536425cba75d9e56fff6972d1a2d/pymongo-4.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:c80e4baee9746f1239cc5b83bb983c01a04769761adb8f462ce0a1902f7cd8bd", size = 821727, upload-time = "2026-09-24T16:58:17.395Z" },
{ url = "https://files.pythonhosted.org/packages/f8/b3/c6f190cc19b873d9aa9e261ee184ae5a605e1d21d4bd3105aa9d52943f86/pymongo-4.18.2-cp310-cp310-win_arm64.whl", hash = "sha256:045f16e6b87b832b37f3acf487c48308bc6bb171bc8b4a3e97f156cf07de14b4", size = 815866, upload-time = "2026-09-24T16:58:19.258Z" },
{ url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" },
{ url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" },
{ url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" },
{ url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" },
{ url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" },
{ url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" },
{ url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" },
{ url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" },
{ url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" },
{ url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" },
{ url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" },
{ url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" },
{ url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" },
{ url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" },
{ url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" },
{ url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" },
{ url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" },
{ url = "https://files.pythonhosted.org/packages/c7/be/82ca9a311b8a89e918c7387d96135786e1edd2eedca43d62dffcb3af5846/pymongo-4.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3be3633a89888fc7443a5a416f74c66216839a5b7b550daaec4da0b1a7283be", size = 818977, upload-time = "2026-09-24T16:58:51.109Z" },
{ url = "https://files.pythonhosted.org/packages/43/38/4817e5ce653ed70162d1412cc74d15c29e88208cea7510967707e908a145/pymongo-4.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd5ecb6d6fab1c5114e66477b1d60b0f3926975c41cf5508d38b28dd0eb6676e", size = 819319, upload-time = "2026-09-24T16:58:52.703Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e9/b8f108ce7c47619e531e0f4a0963178eb9caa5c5da36f4454df8ae6fd15b/pymongo-4.18.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63322fa1499d855a6e63673f8d183fc2d7ecd966702f73150b21e04e01dc0144", size = 1039779, upload-time = "2026-09-24T16:58:54.292Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/3a8394386d485c9c9e518c4e15dce91e9470c3a8b3a0a19e289eb3b9d262/pymongo-4.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979d1e88a20fe9d652f3db57fff55052ddb9022ac346ea55a32a8cfe4b56f90d", size = 1050543, upload-time = "2026-09-24T16:58:55.907Z" },
{ url = "https://files.pythonhosted.org/packages/68/7d/84962f4d8fe0f9e3f112c6b630c7ad9a9f74acd7865ace9ba1e82e1f096a/pymongo-4.18.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef9007562b907a0db6b158ae9e08510678991cc10ad1471de3635f9be737a68e", size = 1075500, upload-time = "2026-09-24T16:58:57.424Z" },
{ url = "https://files.pythonhosted.org/packages/7b/4e/b20ffbc001958d597970567e9231664b8f4c68765376a349834577892fe3/pymongo-4.18.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b41d460c2281874232e6b4a5834bc3fb6cc718d80b6097c38d87ff19ee60898", size = 1067941, upload-time = "2026-09-24T16:58:59.014Z" },
{ url = "https://files.pythonhosted.org/packages/b4/ba/535911b6b35427e3fd31c17a89c9851c024df81779fe6dd473a4f0cdd0fd/pymongo-4.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a57f28271cb325574bd12ad7b3747e545c5b7f560de9f175af7db7be0655639", size = 1049780, upload-time = "2026-09-24T16:59:00.716Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d2/f37bdbbc1a11955dd2609d622aae6722c6cfd9f67528a62c8f80f4db6609/pymongo-4.18.2-cp313-cp313-win32.whl", hash = "sha256:c3f0adbefa29c772f4ebf09a6ac65426405435de199828bedc9534f182ad2fe3", size = 815166, upload-time = "2026-09-24T16:59:02.434Z" },
{ url = "https://files.pythonhosted.org/packages/5a/8a/dc8dac16d8a1c72008a1707046db333a4adaf3dec712c4433ac234e7890d/pymongo-4.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:84f99563560a94321b7c3eb4a3bd0dc81f29a2f67a1da065f78641eff42f4a81", size = 821994, upload-time = "2026-09-24T16:59:04.127Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c9/979b68292e3229a924280ba0c8d5fe4f92604271aa49ba09e497af4aa463/pymongo-4.18.2-cp313-cp313-win_arm64.whl", hash = "sha256:f319857f858a17e1c0e48159b65c89abe93d5ebf3423bb3e3891d8b67a68accd", size = 815730, upload-time = "2026-09-24T16:59:05.629Z" },
]
[[package]]
@@ -5315,11 +5308,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.7.0"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
{ url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]