mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-27 22:04:20 +00:00
Merge remote-tracking branch 'origin/master' into mintlify/cb114598
This commit is contained in:
76 files changed
+2341
-608
No files matched your search
+7
-7
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: ["}
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+180
-179
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user