Compare commits

..
1137 changed files with 20199 additions and 46093 deletions

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "prowler-plugins",
"description": "Prowler Cloud security and compliance for Claude",
"description": "Prowler Cloud Security for Claude Code",
"owner": {
"name": "Prowler",
"email": "support@prowler.com"
@@ -9,7 +9,7 @@
{
"name": "prowler",
"source": "./claude_plugins/prowler",
"description": "Connect Claude to Prowler Cloud through the Prowler MCP server. Reads providers, scans, findings, compliance results and resources for AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and the other providers listed at https://registry.prowler.com, and returns remediation guidance for failed checks. Writes are limited to Prowler Cloud itself: triggering scans, finding triage, mute rules and the mutelist, providers and integrations. Your cloud resources are not changed by the connector.",
"description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.",
"category": "security",
"homepage": "https://prowler.com"
}
+7 -5
View File
@@ -14,8 +14,8 @@ NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
UI_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST=true
UI_PORT=3000
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Required before the first start, no default is shipped: openssl rand -base64 32
AUTH_SECRET=""
# Google Tag Manager ID (empty/unset ⇒ GTM not loaded, zero egress)
# deprecated, use UI_GOOGLE_TAG_MANAGER_ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
@@ -155,8 +155,10 @@ DJANGO_MANAGE_DB_PARTITIONS=True
DJANGO_TOKEN_SIGNING_KEY=""
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY=""
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
# Fernet key protecting every stored credential. Leave empty to generate it on the
# first start, persisted in _data/api/secrets_encryption.key: keep that file across
# upgrades. To set it yourself: openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY=""
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_SENTRY_DSN=
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
@@ -174,7 +176,7 @@ SENTRY_RELEASE=local
# REO_DEV_CLIENT_ID=
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.48.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.45.0
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
-1
View File
@@ -42,7 +42,6 @@ jobs:
github.com:443
pypi.org:443
files.pythonhosted.org:443
release-assets.githubusercontent.com:443
api.github.com:443
raw.githubusercontent.com:443
@@ -81,7 +81,6 @@ jobs:
auth.docker.io:443
production.cloudflare.docker.com:443
production.cloudfront.docker.com:443
tuf-repo-cdn.sigstore.dev:443
raw.githubusercontent.com:443
objects.githubusercontent.com:443
grype.anchore.io:443
-1
View File
@@ -85,7 +85,6 @@ jobs:
github.com:443
pypi.org:443
files.pythonhosted.org:443
release-assets.githubusercontent.com:443
cli.codecov.io:443
keybase.io:443
raw.githubusercontent.com:443
+4 -7
View File
@@ -38,16 +38,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history: a shallow clone cuts the PR's ancestry, git can no longer exclude commits
# already on the base branch, and TruffleHog scans the cut-off commit as a root (the whole
# tree as added). PRs that merge master in hit this and fail on pre-existing content.
fetch-depth: 0
# PRs only need the diff range; push to master/release walks the new range from event.before.
# 50 is enough headroom for the longest realistic PR/push chain without paying for a full clone.
fetch-depth: 50
persist-credentials: false
- name: Scan diff for secrets with TruffleHog
# Action auto-injects --since-commit/--branch from event payload; passing them in extra_args produces duplicate flags.
uses: trufflesecurity/trufflehog@e76dff8a7e57d6df0c60ad8cbf5d5cba505a8fc9 # v3.99.2
uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3.95.3
with:
# The action runs the `latest` image unless told otherwise: pin the scanner to the action's release.
version: 3.99.2@sha256:47a84bc18a0d04a165498bbbd3bacfd84176661cbc91f8e0f85467f6a771e99a
extra_args: --results=verified,unknown
@@ -77,7 +77,6 @@ jobs:
auth.docker.io:443
production.cloudflare.docker.com:443
production.cloudfront.docker.com:443
tuf-repo-cdn.sigstore.dev:443
ghcr.io:443
pkg-containers.githubusercontent.com:443
files.pythonhosted.org:443
-1
View File
@@ -49,7 +49,6 @@ jobs:
github.com:443
pypi.org:443
files.pythonhosted.org:443
release-assets.githubusercontent.com:443
cli.codecov.io:443
keybase.io:443
ingest.codecov.io:443
+2 -28
View File
@@ -84,8 +84,6 @@ 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
@@ -94,33 +92,8 @@ 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 update-ref "refs/remotes/origin/${BRANCH_NAME}" "$MASTER_SHA"
git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME"
else
echo "ERROR: Branch $BRANCH_NAME does not exist. For patch releases (X.Y.Z), the branch should already exist."
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."
exit 1
fi
@@ -417,3 +390,4 @@ jobs:
if: always()
run: |
rm -f prowler_changelog.md api_changelog.md ui_changelog.md mcp_changelog.md combined_changelog.md
-27
View File
@@ -1,27 +0,0 @@
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}"
-1
View File
@@ -40,7 +40,6 @@ jobs:
github.com:443
pypi.org:443
files.pythonhosted.org:443
release-assets.githubusercontent.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -81,7 +81,6 @@ jobs:
auth.docker.io:443
production.cloudflare.docker.com:443
production.cloudfront.docker.com:443
tuf-repo-cdn.sigstore.dev:443
api.github.com:443
mirror.gcr.io:443
check.trivy.dev:443
-24
View File
@@ -640,30 +640,6 @@ jobs:
flags: prowler-py${{ matrix.python-version }}-e2enetworks
files: ./e2enetworks_coverage.xml
# Huawei Cloud Provider
- name: Check if Huawei Cloud files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-huaweicloud
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
./prowler/**/huaweicloud/**
./tests/**/huaweicloud/**
./uv.lock
- name: Run Huawei Cloud tests
if: steps.changed-huaweicloud.outputs.any_changed == 'true'
run: uv run pytest -n auto --cov=./prowler/providers/huaweicloud --cov-report=xml:huaweicloud_coverage.xml tests/providers/huaweicloud
- name: Upload Huawei Cloud coverage to Codecov
if: steps.changed-huaweicloud.outputs.any_changed == 'true'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler-py${{ matrix.python-version }}-huaweicloud
files: ./huaweicloud_coverage.xml
# External Provider (dynamic loading)
- name: Check if External Provider files changed
if: steps.check-changes.outputs.any_changed == 'true'
@@ -78,7 +78,6 @@ jobs:
auth.docker.io:443
production.cloudflare.docker.com:443
production.cloudfront.docker.com:443
tuf-repo-cdn.sigstore.dev:443
registry.npmjs.org:443
dl-cdn.alpinelinux.org:443
fonts.googleapis.com:443
+1 -10
View File
@@ -39,10 +39,7 @@ jobs:
github.repository == 'prowler-cloud/prowler' &&
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) &&
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
# ubuntu-latest is moving to 26.04 (rollout began October 19, 2026; completion
# targeted for November 19, 2026), where `playwright install` refuses to install
# Chromium for the pinned Playwright. Stay on 24.04 until the suite upgrades.
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
env:
AUTH_SECRET: "fallback-ci-secret-for-testing"
AUTH_TRUST_HOST: true
@@ -394,12 +391,6 @@ jobs:
working-directory: ./ui
run: pnpm run test:e2e:registry
# A worker that crashes on import or a connection test that never resolves
# only shows up here; Playwright just reports a missing footer button.
- name: Dump API service logs
if: failure()
run: docker compose logs --no-color --tail=300 api worker worker-beat || true
- name: Upload test reports
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: failure()
+5 -4
View File
@@ -21,10 +21,7 @@ permissions: {}
jobs:
ui-tests:
# ubuntu-latest is moving to 26.04 (rollout began October 19, 2026; completion
# targeted for November 19, 2026), where `playwright install` refuses to install
# Chromium for the pinned Playwright. Stay on 24.04 until the suite upgrades.
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
@@ -139,6 +136,10 @@ jobs:
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run healthcheck
- name: Check product-tour alignment
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run tour:check
- name: Run pnpm audit
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run audit
+54 -30
View File
@@ -7,19 +7,66 @@
ignore:
# Modules compiled into the Trivy binary we ship.
# Only a Trivy rebuild by its vendor can change these; the version is pinned in our Dockerfile.
# CVE-2026-71556 is the same temporary exception documented in .trivyignore.yaml:
# Trivy 0.73.0 still embeds go-git 5.19.1, while the 5.19.2 fix is merged only on
# Trivy main. Remove this entry with the Trivy exception by 2026-09-15.
# https://github.com/aquasecurity/trivy/blob/v0.73.0/go.mod#L46
# https://github.com/aquasecurity/trivy/commit/a2edba9a03987ba0d2ebc8212c1a9a1e6979497b
- vulnerability: CVE-2026-71556
package:
name: github.com/go-git/go-git/v5
# CVE-2026-84304 is the same temporary exception documented in .trivyignore.yaml:
# Trivy 0.74.0 still embeds grpc 1.82.1, while the 1.83.1 fix is not in any release.
# Prowler only runs `trivy image` / `trivy fs`, never client/server mode, so no gRPC
# endpoint exists in the image. Pinned to the embedded version so the rule stops
# matching on its own once Trivy bumps grpc. Remove with the Trivy exception by 2026-10-15.
# https://github.com/aquasecurity/trivy/pull/11176
- vulnerability: CVE-2026-84304
package:
name: google.golang.org/grpc
version: v1.82.1
# CVE-2026-84445 is the same temporary exception documented in .trivyignore.yaml:
# Trivy 0.74.0 still embeds grpc 1.82.1, while the 1.82.2 / 1.83.2 fix is not in any
# release. The panic needs a gRPC server built with `xds.NewGRPCServer()`; Prowler only
# runs `trivy image` / `trivy fs`, so the image serves no gRPC at all. Pinned to the
# embedded version so the rule stops matching on its own once Trivy bumps grpc. Remove
# with the Trivy exception by 2026-10-15.
# https://github.com/advisories/GHSA-2v4p-qf9q-27wj
- vulnerability: CVE-2026-84445
package:
name: google.golang.org/grpc
version: v1.82.1
# CVE-2026-56855 / CVE-2026-78662 are the same temporary exception documented in
# .trivyignore.yaml: Trivy 0.74.0 still embeds golang.org/x/crypto v0.55.0, while the
# 0.56.0 fix (published 2026-09-02) hasn't reached any Trivy release, or even Trivy
# main, yet. Pinned to the embedded version so the rule stops matching on its own once
# Trivy bumps it. Remove with the Trivy exception by 2026-10-15.
- vulnerability: CVE-2026-56855
package:
name: golang.org/x/crypto
version: v0.55.0
- vulnerability: CVE-2026-78662
package:
name: golang.org/x/crypto
version: v0.55.0
- vulnerability: CVE-2026-56852
package:
name: golang.org/x/text
- vulnerability: GHSA-hrxh-6v49-42gf
package:
name: google.golang.org/grpc
- vulnerability: CVE-2026-50151
package:
name: oras.land/oras-go/v2
# Shipped inside the PowerShell tarball, in its bundled MicrosoftTeams module.
# Not a dependency we declare, and not one we can upgrade independently.
- vulnerability: CVE-2026-26127
package:
name: Microsoft.Bcl.Memory
# Documented in .trivyignore.yaml: ships inside ExchangeOnlineManagement
# 3.10.1, the latest gallery release. Re-check on the next EXO release.
- vulnerability: CVE-2026-84008
package:
name: System.Security.Cryptography.Pkcs
version: 10.0.0
# The .NET runtime bundled inside the PowerShell tarball the Dockerfile pins.
# CVE-2026-62901 is the same temporary exception documented in .trivyignore.yaml:
# fixed in .NET 9.0.19 / 10.0.11 (2026-08-11), but no published PowerShell release
@@ -79,26 +126,3 @@ ignore:
- vulnerability: CVE-2026-82049
package:
name: python
# zlib in the Alpine base image of the MCP (python:3.13.16-alpine3.23) container.
# CVE-2026-85091 is a heap overflow in gz_vacate() reached only through non-blocking
# gzwrite() followed by gzprintf() after a write stall; the MCP server never writes
# gzip files, so the image never calls that path. The UI image no longer needs this:
# node:24.22.1-alpine is built on Alpine 3.24 and ships 1.3.2-r1. TEMPORARY: the fix
# landed in the 3.23 index on 2026-10-06, two days after the pinned python image was
# built, and could be taken with `apk add --upgrade`, but we wait for the base image
# to ship it instead (python:3.13.17-alpine3.23, built 2026-10-22, already does).
# Pinned to that version so the rule stops matching on its own once the base image
# moves forward. Remove when the MCP digest is 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
# Documented in .trivyignore.yaml: go-jose 4.1.5 compiled into the Trivy binary (0.76.1,
# the latest release). Pinned to 4.1.5 so it stops matching with the next Trivy bump.
- vulnerability: CVE-2026-116587
package:
name: github.com/go-jose/go-jose/v4
version: v4.1.5
+69 -57
View File
@@ -77,9 +77,6 @@ vulnerabilities:
# 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"
@@ -108,23 +105,6 @@ vulnerabilities:
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
# Same manifest, pinned to 8.0.4: PowerShell's own copy is also flagged by these CVEs.
- id: CVE-2026-96222
purls:
- "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
expired_at: 2027-01-31
- id: CVE-2026-96958
purls:
- "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
expired_at: 2027-01-31
- id: CVE-2026-96959
purls:
- "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
expired_at: 2027-01-31
- id: CVE-2026-96960
purls:
- "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
expired_at: 2027-01-31
- id: CVE-2026-13676
purls:
- "pkg:npm/fast-uri"
@@ -193,44 +173,76 @@ vulnerabilities:
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
# Two more SDK declarations and follow-redirects 1.16.0 from the same manifest. The SDK
# entries stay pinned to 1.27.1 for the reason above. No lock in this repo lists
# follow-redirects, so its pin only keeps a future real dependency from being hidden.
- id: CVE-2026-111767
purls:
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
- id: CVE-2026-111768
purls:
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
- id: CVE-2026-111755
purls:
- "pkg:npm/follow-redirects@1.16.0"
expired_at: 2027-01-31
- id: CVE-2026-111756
purls:
- "pkg:npm/follow-redirects@1.16.0"
expired_at: 2027-01-31
# Unlike the manifest entries above, this DLL ships in the image:
# ExchangeOnlineManagement/3.10.1/netCore/System.Security.Cryptography.Pkcs.dll. It is a
# DoS in CMS/PKCS parsing; Prowler only feeds it the tenant's own certificate. 3.10.1 is
# the latest gallery release, so only Microsoft can update it. Re-check on the next EXO
# release.
- id: CVE-2026-84008
# 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
purls:
- "pkg:nuget/System.Security.Cryptography.Pkcs@10.0.0"
expired_at: 2026-12-15
- "pkg:golang/google.golang.org/grpc"
expired_at: 2026-10-15
# go-jose 4.1.5 is compiled into the Trivy binary (usr/local/bin/trivy). Trivy merged the
# 4.1.6 bump upstream (aquasecurity/trivy#11488) but 0.76.1 is still the latest release.
# Bump TRIVY_VERSION and drop this entry once a release ships it.
- id: CVE-2026-116587
# 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
purls:
- "pkg:golang/github.com/go-jose/go-jose/v4@v4.1.5"
expired_at: 2026-11-20
- "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
- id: CVE-2026-56852
purls:
- "pkg:golang/golang.org/x/text"
expired_at: 2026-12-31
- id: GHSA-hrxh-6v49-42gf
purls:
- "pkg:golang/google.golang.org/grpc"
expired_at: 2026-12-31
- id: CVE-2026-50151
purls:
- "pkg:golang/oras.land/oras-go/v2"
expired_at: 2026-12-31
- id: CVE-2026-50163
purls:
- "pkg:golang/oras.land/oras-go/v2"
expired_at: 2026-12-31
+6
View File
@@ -51,6 +51,7 @@ Use these skills for detailed patterns on-demand:
| `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) |
| `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) |
| `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) |
| `prowler-tour` | Keep product-tour definitions aligned with the UI | [SKILL.md](skills/prowler-tour/SKILL.md) |
| `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) |
| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
@@ -68,10 +69,12 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Adding new providers | `prowler-provider` |
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
| Adding services to existing providers | `prowler-provider` |
| Adding, updating, or removing a tour definition (*.tour.ts) | `prowler-tour` |
| After creating/modifying a skill | `skill-sync` |
| App Router / Server Actions | `nextjs-16` |
| Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` |
| Building AI chat features | `ai-sdk-5` |
| Changing button labels or section headings on a tour-covered page | `prowler-tour` |
| Committing changes | `prowler-commit` |
| Configuring MCP servers in agentic workflows | `gh-aw` |
| Create PR that requires changelog entry | `prowler-changelog` |
@@ -91,6 +94,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Creating/updating compliance frameworks | `prowler-compliance` |
| Debug why a GitHub Actions job is failing | `prowler-ci` |
| Debugging gh-aw compilation errors | `gh-aw` |
| Editing a UI file containing data-tour-id attributes | `prowler-tour` |
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
| Fixing bug | `tdd` |
| Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` |
@@ -107,6 +111,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` |
| Refactoring code | `tdd` |
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
| Renaming or removing a data-tour-id attribute value | `prowler-tour` |
| Restructuring routes or layouts covered by a tour | `prowler-tour` |
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
+11 -14
View File
@@ -1,24 +1,24 @@
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
ARG POWERSHELL_VERSION=7.5.12
ARG POWERSHELL_VERSION=7.5.11
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.76.1
ARG TRIVY_VERSION=0.74.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=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
ARG TRIVY_SHA256_ARM64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
@@ -30,7 +30,6 @@ ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed9
# perl-base 5.40.1-6+deb13u1 CVE-2026-42497, -48962, -57432
# libssh2-1t64 1.11.1-1+deb13u2 CVE-2026-58050
# libpcre2-8-0 10.46-1~deb13u2 CVE-2026-86145, -89161
# libbrotli1 1.1.0-2+deb13u1 CVE-2026-111497
# Taken as a targeted --only-upgrade rather than by moving the digest: the newest
# published python:3.12-slim-trixie carries the same vulnerable versions. The three
# openssl packages are flagged separately, so all are named.
@@ -41,7 +40,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config libzstd-dev zlib1g-dev \
&& apt-get install -y --no-install-recommends --only-upgrade \
util-linux libssl3t64 openssl openssl-provider-legacy \
libsqlite3-0 gzip perl-base libssh2-1t64 libpcre2-8-0 libbrotli1 \
libsqlite3-0 gzip perl-base libssh2-1t64 libpcre2-8-0 \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
@@ -122,15 +121,13 @@ ENV HOME='/home/prowler'
ENV PATH="${HOME}/.local/bin:${PATH}"
#hadolint ignore=DL3013
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir uv==0.13.4
pip install --no-cache-dir uv==0.12.0
RUN uv sync --locked --compile-bytecode && \
rm -rf ~/.cache/uv
# Install PowerShell modules. Drop the MicrosoftTeams build SBOM: Trivy reports the npm
# tooling it declares as installed, though none ships (see .trivyignore.yaml).
RUN .venv/bin/python prowler/providers/m365/lib/powershell/m365_powershell.py && \
rm -rf "${HOME}"/.local/share/powershell/Modules/MicrosoftTeams/*/_manifest
# Install PowerShell modules
RUN .venv/bin/python prowler/providers/m365/lib/powershell/m365_powershell.py
USER root
+5
View File
@@ -185,6 +185,8 @@ VERSION=$(curl -s https://api.github.com/repos/prowler-cloud/prowler/releases/la
curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/docker-compose.yml"
# Environment variables can be customized in the .env file. Using default values in production environments is not recommended.
curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/.env"
# AUTH_SECRET is not shipped: set it in .env before the first start
perl -pi -e "s|^AUTH_SECRET=.*|AUTH_SECRET=\"$(openssl rand -base64 32)\"|" .env
docker compose up -d
```
@@ -195,6 +197,9 @@ $VERSION = (Invoke-RestMethod -Uri "https://api.github.com/repos/prowler-cloud/p
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/$VERSION/docker-compose.yml" -OutFile "docker-compose.yml"
# Environment variables can be customized in the .env file. Using default values in production environments is not recommended.
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/$VERSION/.env" -OutFile ".env"
# AUTH_SECRET is not shipped: set it in .env before the first start
$AuthSecret = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
(Get-Content .env) -replace '^AUTH_SECRET=.*', "AUTH_SECRET=`"$AuthSecret`"" | Set-Content .env
docker compose up -d
```
-49
View File
@@ -4,55 +4,6 @@ All notable changes to the **Prowler API** are documented in this file.
<!-- changelog: release notes start -->
## [1.48.0] (Prowler v5.47.0)
### 🔐 Security
- pymongo to 4.18.3, dnspython to 2.9.0 and oauthlib to 4.0.0 in the API container image, with django-allauth from 65.15.0 to 65.19.7 because earlier releases cap oauthlib below 4, patching the BSON decoding and DNS compression pointer denial of service advisories and the OAuth device flow token disclosure [(#13185)](https://github.com/prowler-cloud/prowler/pull/13185)
---
## [1.47.0] (Prowler v5.46.0)
### 🐞 Fixed
- Compliance report PDF cover table wraps every value, so long account IDs such as OCI tenancy OCIDs no longer overflow their cell [(#13063)](https://github.com/prowler-cloud/prowler/pull/13063)
- Scan ingestion compares the serialized resource metadata with the stored value and serializes sets in a stable order, so unchanged resources no longer get their metadata, details and updated_at rewritten on every scan [(#13148)](https://github.com/prowler-cloud/prowler/pull/13148)
- Report generation shares the parsed resource metadata between the findings of a batch and releases each batch before building the next one [(#13149)](https://github.com/prowler-cloud/prowler/pull/13149)
### 🔐 Security
- `GET /resources/metadata`, `GET /resources/metadata/latest`, `GET /findings/metadata` and `GET /findings/metadata/latest` now honour the provider groups of the caller role instead of aggregating every provider in the tenant [(#12964)](https://github.com/prowler-cloud/prowler/pull/12964)
- Trivy from 0.75.0 to 0.76.0 in the API container image, built with Go 1.27.2 and x/net 0.60.0, patching the HTTP/2 and TLS CVEs bundled in Trivy 0.75.0 [(#13064)](https://github.com/prowler-cloud/prowler/pull/13064)
- Pillow from 12.3.0 to 13.0.0, certifi to 2026.10.19, aiohttp to 3.14.5 with yarl 1.25.1, uv to 0.13.4 and Trivy to 0.76.1 in the API container image, patching the Pillow JPEG decoder heap overflow, the compromised emSign root CAs bundled in certifi and uv, the aiohttp resource exhaustion advisories and the Go 1.27.2 stdlib CVE in the Trivy binary [(#13107)](https://github.com/prowler-cloud/prowler/pull/13107)
- Attack Paths custom queries strip block comments before the blocked-operation check and drop APOC virtual nodes and relationships from results, so `LOAD CSV` can no longer be split with a comment nor its response returned under a forged provider label [(#13120)](https://github.com/prowler-cloud/prowler/pull/13120)
- SAML SSO configuration endpoints require the `manage_account` permission instead of `manage_integrations`, since the configured IdP controls who signs in and which role they receive [(#13121)](https://github.com/prowler-cloud/prowler/pull/13121)
- djangorestframework from 3.15.2 to 3.18.3 in the API, patching the JSON parser charset and Accept header denial of service and the TokenProxy revocation bypass, and the API container image no longer ships the MicrosoftTeams build SBOM that Trivy read as installed zod and body-parser [(#13157)](https://github.com/prowler-cloud/prowler/pull/13157)
---
## [1.46.0] (Prowler v5.45.0)
### 🐞 Fixed
- OCI provider secrets keep the region as home region for credential validation and scans, instead of always using us-ashburn-1 [(#12865)](https://github.com/prowler-cloud/prowler/pull/12865)
- Gunicorn workers are now recycled after 10000 requests (plus up to 1000 of jitter) to cap memory growth, configurable with `GUNICORN_MAX_REQUESTS` and `GUNICORN_MAX_REQUESTS_JITTER` [(#13047)](https://github.com/prowler-cloud/prowler/pull/13047)
### 🔐 Security
- `pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9 [(#12911)](https://github.com/prowler-cloud/prowler/pull/12911)
- Sentry error events no longer include user identity, IP addresses, cookies, headers or request bodies, which could contain personal data or provider credentials [(#12912)](https://github.com/prowler-cloud/prowler/pull/12912)
- 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 [(#12952)](https://github.com/prowler-cloud/prowler/pull/12952)
- `django` from 5.1.15 to 5.2.17, patching CVE-2026-15307 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
- `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 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
- `urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
- `pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749 [(#12955)](https://github.com/prowler-cloud/prowler/pull/12955)
- `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 [(#12965)](https://github.com/prowler-cloud/prowler/pull/12965)
- 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 [(#13009)](https://github.com/prowler-cloud/prowler/pull/13009)
- PowerShell from 7.5.11 to 7.5.12 in the API container image, bundling .NET runtime 9.0.21 [(#13037)](https://github.com/prowler-cloud/prowler/pull/13037)
---
## [1.45.0] (Prowler v5.44.0)
### 🚀 Added
+10 -12
View File
@@ -1,23 +1,23 @@
FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build
LABEL maintainer="https://github.com/prowler-cloud/api"
ARG POWERSHELL_VERSION=7.5.12
ARG POWERSHELL_VERSION=7.5.11
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.76.1
ARG TRIVY_VERSION=0.74.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=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
ARG TRIVY_SHA256_ARM64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5
ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
@@ -125,7 +125,7 @@ RUN mkdir -p /tmp/prowler_api_output
COPY --chown=prowler:prowler pyproject.toml uv.lock ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir uv==0.13.4
pip install --no-cache-dir uv==0.12.0
ENV PATH="/home/prowler/.local/bin:$PATH"
@@ -134,10 +134,8 @@ RUN uv sync --locked --no-install-project && \
rm -rf ~/.cache/uv
# Invoked as a module so the base image's Python minor version is not baked
# into a site-packages path. Drop the MicrosoftTeams build SBOM: Trivy reports the npm
# tooling it declares as installed, though none ships (see .trivyignore.yaml).
RUN .venv/bin/python -m prowler.providers.m365.lib.powershell.m365_powershell && \
rm -rf /home/prowler/.local/share/powershell/Modules/MicrosoftTeams/*/_manifest
# into a site-packages path.
RUN .venv/bin/python -m prowler.providers.m365.lib.powershell.m365_powershell
USER root
@@ -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 @@
OCI provider secrets keep the region as home region for credential validation and scans, instead of always using us-ashburn-1
+1
View File
@@ -0,0 +1 @@
`pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9
@@ -0,0 +1 @@
`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749
@@ -0,0 +1 @@
PostgreSQL, Valkey and Neo4j ports in the self-hosted `docker-compose.yml` bind to `127.0.0.1` instead of every host interface, and `worker` and `worker-beat` share the API key directory
@@ -0,0 +1 @@
API refuses to start with a JWT signing key published in this repository, and the Helm minimal-installation example no longer ships working secrets
@@ -0,0 +1 @@
`DJANGO_SECRETS_ENCRYPTION_KEY` and `SECRET_KEY` are generated on the first start and persisted under `_data/api` when empty, the API refuses to start with an encryption key that was ever published in this repository, and the self-hosted `.env` no longer ships working values for `DJANGO_SECRETS_ENCRYPTION_KEY` and `AUTH_SECRET`
@@ -0,0 +1 @@
Sentry error events no longer include user identity, IP addresses, cookies, headers or request bodies, which could contain personal data or provider credentials
@@ -0,0 +1 @@
`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689
+12 -12
View File
@@ -26,7 +26,7 @@ dependencies = [
"celery (==5.6.2)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
"django (==5.2.17)",
"django-allauth[saml] (==65.19.7)",
"django-allauth[saml] (==65.15.0)",
"django-celery-beat (==2.9.0)",
"django-celery-results (==2.6.0)",
"django-cors-headers==4.4.0",
@@ -34,7 +34,7 @@ dependencies = [
"django-filter==24.3",
"django-guid==3.5.0",
"django-postgres-extra (==2.0.9)",
"djangorestframework==3.18.3",
"djangorestframework==3.15.2",
"djangorestframework-jsonapi==7.0.2",
"djangorestframework-simplejwt (==5.5.1)",
"drf-nested-routers (==0.95.0)",
@@ -71,7 +71,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.49.0"
version = "1.46.0"
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
# target-version tracks this project's lowest supported Python.
@@ -100,7 +100,7 @@ constraint-dependencies = [
"aiobotocore==2.25.1",
"aiofiles==24.1.0",
"aiohappyeyeballs==2.6.1",
"aiohttp==3.14.5",
"aiohttp==3.14.3",
"aioitertools==0.13.0",
"aiosignal==1.4.0",
"alibabacloud-actiontrail20200706==2.4.1",
@@ -196,7 +196,7 @@ constraint-dependencies = [
"botocore==1.40.61",
"cartography==0.138.1",
"celery==5.6.2",
"certifi==2026.10.19",
"certifi==2026.1.4",
"cffi==2.0.0",
"charset-normalizer==3.4.4",
"circuitbreaker==2.1.3",
@@ -223,7 +223,7 @@ constraint-dependencies = [
"distro==1.9.0",
"dj-rest-auth==7.0.1",
"django==5.2.17",
"django-allauth==65.19.7",
"django-allauth==65.15.0",
"django-celery-beat==2.9.0",
"django-celery-results==2.6.0",
"django-cors-headers==4.4.0",
@@ -234,10 +234,10 @@ constraint-dependencies = [
"django-postgres-extra==2.0.9",
"django-silk==5.3.2",
"django-timezone-field==7.2.1",
"djangorestframework==3.18.3",
"djangorestframework==3.15.2",
"djangorestframework-jsonapi==7.0.2",
"djangorestframework-simplejwt==5.5.1",
"dnspython==2.9.0",
"dnspython==2.8.0",
"docker==7.1.0",
"dogpile-cache==1.5.0",
"dparse==0.6.4",
@@ -337,7 +337,7 @@ constraint-dependencies = [
"nest-asyncio==1.6.0",
"nltk==3.9.4",
"numpy==2.2.6",
"oauthlib==4.0.0",
"oauthlib==3.3.1",
"oci==2.184.1",
"openai==1.109.1",
"openstacksdk==4.2.0",
@@ -350,7 +350,7 @@ constraint-dependencies = [
"pagerduty==6.1.0",
"pandas==2.2.3",
"pbr==7.0.3",
"pillow==13.0.0",
"pillow==12.3.0",
"pkginfo==1.12.1.2",
"platformdirs==4.5.1",
"plotly==6.5.2",
@@ -453,7 +453,7 @@ constraint-dependencies = [
"xlsxwriter==3.2.9",
"xmlsec==1.3.17",
"xmltodict==1.0.2",
"yarl==1.25.1",
"yarl==1.22.0",
"zipp==3.23.0",
"zope-event==6.1",
"zope-interface==8.2",
@@ -485,7 +485,7 @@ 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.3",
"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
+170
View File
@@ -1,32 +1,84 @@
import hashlib
import logging
import os
import sys
import time
from pathlib import Path
from config.custom_logging import BackendLogger
from config.env import env
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
logger = logging.getLogger(BackendLogger.API)
SIGNING_KEY_ENV = "DJANGO_TOKEN_SIGNING_KEY"
VERIFYING_KEY_ENV = "DJANGO_TOKEN_VERIFYING_KEY"
ENCRYPTION_KEY_ENV = "DJANGO_SECRETS_ENCRYPTION_KEY"
SECRET_KEY_ENV = "SECRET_KEY"
PRIVATE_KEY_FILE = "jwt_private.pem"
PUBLIC_KEY_FILE = "jwt_public.pem"
ENCRYPTION_KEY_FILE = "secrets_encryption.key"
SECRET_KEY_FILE = "django_secret.key"
KEYS_DIRECTORY = (
Path.home() / ".config" / "prowler-api"
) # `/home/prowler/.config/prowler-api` inside the container
# SHA-256 of encryption keys that were committed to this repository with a working
# value. They are public, so a deployment still using one of them must rotate.
PUBLISHED_ENCRYPTION_KEY_DIGESTS = frozenset(
{
# Root `.env` shipped to self-hosted installs until this check was added
"a9c1d15afaddc1753245e48117f7c80525973a4f64179d447a843ca372c0ba0e",
# contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml
"c7576d54c7120bab3a486d44a7a8640907cc28eaeec6bc41f4aa2e95ad407015",
}
)
# SHA-256 of the base64 body of JWT signing keys that were committed to this
# repository with a working value. Anyone holding one can forge tokens.
PUBLISHED_SIGNING_KEY_DIGESTS = frozenset(
{
# contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml
"375d5755ab4a7d38c58b38c67c1a6d1cab5afc91844445d8489c15ce7779d949",
}
)
ENCRYPTION_KEY_DOCS = (
"https://docs.prowler.com/getting-started/installation/prowler-app"
"#secrets-encryption-key"
)
_keys_initialized = False # Flag to prevent multiple executions within the same process
def _digest(value):
return hashlib.sha256(value.encode()).hexdigest()
def _pem_digest(value):
"""Digest of a PEM's base64 body, so indentation and line wrapping do not matter."""
body = "".join(
line.strip()
for line in value.splitlines()
if line.strip() and "-----" not in line
)
return _digest(body)
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"
def import_models(self):
# `api.models` builds its Fernet instance at import time, so the encryption
# key has to be settled before Django loads this app's models.
self._ensure_secrets()
super().import_models()
def ready(self):
from api import (
schema_extensions, # noqa: F401
@@ -42,11 +94,119 @@ class ApiConfig(AppConfig):
):
self._ensure_crypto_keys()
def _ensure_secrets(self):
"""
Settle the symmetric secrets every process needs before the models load:
- `DJANGO_SECRETS_ENCRYPTION_KEY`, the Fernet key protecting stored credentials
- `SECRET_KEY`, Django's signing key
Both are generated and persisted under `KEYS_DIRECTORY` when empty. An
encryption key that was ever published with this repository is refused.
"""
if getattr(settings, "TESTING", False):
return
self._ensure_encryption_key()
self._ensure_secret_key()
def _ensure_encryption_key(self):
key = (getattr(settings, "SECRETS_ENCRYPTION_KEY", "") or "").strip()
if key:
if _digest(key) in PUBLISHED_ENCRYPTION_KEY_DIGESTS:
raise ImproperlyConfigured(
f"'{ENCRYPTION_KEY_ENV}' is set to a value that was published in the "
"Prowler repository and is therefore public. Generate a new key, "
f"re-enter the stored credentials and restart. See {ENCRYPTION_KEY_DOCS}"
)
return
key = self._load_or_generate_secret(
ENCRYPTION_KEY_FILE, ENCRYPTION_KEY_ENV, self._generate_encryption_key
)
os.environ[ENCRYPTION_KEY_ENV] = key
settings.SECRETS_ENCRYPTION_KEY = key
# Mutated in place: `drf_simple_apikey` keeps a reference to this dict
settings.DRF_API_KEY["FERNET_SECRET"] = key
def _ensure_secret_key(self):
# Read from the environment: `settings.SECRET_KEY` raises while it is empty
if env.str(SECRET_KEY_ENV, default="").strip():
return
key = self._load_or_generate_secret(
SECRET_KEY_FILE, SECRET_KEY_ENV, self._generate_secret_key
)
os.environ[SECRET_KEY_ENV] = key
settings.SECRET_KEY = key
def _load_or_generate_secret(self, file_name, env_name, generate):
"""
Return the secret persisted as `file_name`, generating it on first boot.
"""
existing = self._read_key_file(file_name)
if existing:
return existing
file_path = KEYS_DIRECTORY / file_name
value = generate()
try:
file_path.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
# Another process of this deployment generated it first
return self._wait_for_key_file(file_name, env_name)
except OSError as e:
logger.error(f"Cannot persist '{file_name}' under '{KEYS_DIRECTORY}': {e}")
raise ImproperlyConfigured(
f"'{env_name}' is empty and no value could be persisted under "
f"'{KEYS_DIRECTORY}'. Set '{env_name}' in the environment or make the "
"directory writable."
) from e
with os.fdopen(fd, "w") as key_file:
key_file.write(value)
logger.warning(
f"'{env_name}' was empty: generated '{file_name}' under '{KEYS_DIRECTORY}'. "
"Keep that file across upgrades and back it up; losing it makes the data "
"it protects unreadable."
)
return value
def _wait_for_key_file(self, file_name, env_name):
for _ in range(10):
existing = self._read_key_file(file_name)
if existing:
return existing
time.sleep(0.1)
raise ImproperlyConfigured(
f"'{env_name}' is empty and '{file_name}' under '{KEYS_DIRECTORY}' could "
"not be read. Set the variable in the environment or remove the file."
)
@staticmethod
def _generate_encryption_key():
from cryptography.fernet import Fernet
return Fernet.generate_key().decode()
@staticmethod
def _generate_secret_key():
from django.core.management.utils import get_random_secret_key
return get_random_secret_key()
def _ensure_crypto_keys(self):
"""
Orchestrator method that ensures all required cryptographic keys are present.
This method coordinates the generation of:
- RSA key pairs for JWT token signing and verification
The symmetric secrets are handled earlier, in `_ensure_secrets`, because the
models need them at import time.
Note: During development, Django spawns multiple processes (migrations, fixtures, etc.)
which will each generate their own keys. This is expected behavior and each process
will have consistent keys for its lifetime. In production, set the keys as environment
@@ -66,6 +226,16 @@ class ApiConfig(AppConfig):
signing_key = env.str(SIGNING_KEY_ENV, default="").strip()
verifying_key = env.str(VERIFYING_KEY_ENV, default="").strip()
if signing_key and _pem_digest(signing_key) in PUBLISHED_SIGNING_KEY_DIGESTS:
raise ImproperlyConfigured(
f"'{SIGNING_KEY_ENV}' is set to a key pair that was published in the "
"Prowler repository and is therefore public. Anyone holding it can "
"forge API tokens. Generate a new pair with 'openssl genrsa -out "
"private.pem 2048' and 'openssl rsa -in private.pem -pubout', set both "
"variables and restart. Existing sessions end and users sign in again; "
"no stored data needs re-encrypting."
)
if not signing_key or not verifying_key:
logger.info(
f"Generating JWT RSA key pair. In production, set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' "
@@ -12,7 +12,7 @@ Two responsibilities:
Label-injection pipeline:
1. **Protect** string literals and comments (placeholder replacement).
1. **Protect** string literals and line comments (placeholder replacement).
2. **Split** by top-level clause keywords to track clause context.
3. **Pass A** - inject into *labeled* node patterns in ALL segments.
4. **Pass B** - inject into *bare* node patterns in MATCH segments only.
@@ -25,12 +25,10 @@ from rest_framework.exceptions import ValidationError
from tasks.jobs.attack_paths.config import get_provider_label
# Step 1 - String / comment protection
# Single combined regex: strings first, then line and block comments
# Single combined regex: strings first, then line comments
# The regex engine finds the leftmost match, so a string like 'https://prowler.com'
# is consumed as a string before the // inside it can match as a comment
_PROTECTED_RE = re.compile(
r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"|//[^\n]*|/\*[\s\S]*?\*/"
)
_PROTECTED_RE = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"|//[^\n]*")
# Step 2 - Clause splitting
# `OPTIONAL MATCH` must come before `MATCH` to avoid partial matching
@@ -145,26 +143,18 @@ def inject_label(cypher: str, label: str) -> str:
# Patterns that indicate SSRF or dangerous procedure calls
# Defense-in-depth layer - the primary control is `neo4j.READ_ACCESS`
# Neo4j accepts whitespace (and therefore comments) around the dots of a
# qualified procedure name, so `apoc . load` is `apoc.load`.
_BLOCKED_APOC_NAMESPACES = (
"load",
"import",
"export",
"cypher",
"systemdb",
"config",
"periodic",
"do",
"trigger",
"custom",
)
_BLOCKED_PATTERNS = [
re.compile(r"\bLOAD\s+CSV\b", re.IGNORECASE),
*(
re.compile(rf"\bapoc\s*\.\s*{namespace}\b", re.IGNORECASE)
for namespace in _BLOCKED_APOC_NAMESPACES
),
re.compile(r"\bapoc\.load\b", re.IGNORECASE),
re.compile(r"\bapoc\.import\b", re.IGNORECASE),
re.compile(r"\bapoc\.export\b", re.IGNORECASE),
re.compile(r"\bapoc\.cypher\b", re.IGNORECASE),
re.compile(r"\bapoc\.systemdb\b", re.IGNORECASE),
re.compile(r"\bapoc\.config\b", re.IGNORECASE),
re.compile(r"\bapoc\.periodic\b", re.IGNORECASE),
re.compile(r"\bapoc\.do\b", re.IGNORECASE),
re.compile(r"\bapoc\.trigger\b", re.IGNORECASE),
re.compile(r"\bapoc\.custom\b", re.IGNORECASE),
]
@@ -172,11 +162,10 @@ def validate_custom_query(cypher: str) -> None:
"""Reject queries containing known SSRF or dangerous procedure patterns.
Raises ValidationError if a blocked pattern is found.
String literals and comments are replaced by a space before matching:
literals cannot trigger false positives and a comment cannot split a
keyword pair such as `LOAD /**/ CSV`, which Neo4j still parses as `LOAD CSV`.
String literals and comments are stripped before matching to avoid
false positives.
"""
stripped = _PROTECTED_RE.sub(" ", cypher)
stripped = _PROTECTED_RE.sub("", cypher)
for pattern in _BLOCKED_PATTERNS:
if pattern.search(stripped):
raise ValidationError({"query": "Query contains a blocked operation"})
@@ -45,10 +45,6 @@ class SinkDatabase(Protocol):
parameters: dict[str, Any] | None = None,
) -> neo4j.graph.Graph: ...
def is_persisted_element_id(self, element_id: str) -> bool:
"""Whether an element id belongs to a stored entity rather than one minted at query time."""
return True
def create_database(self, database: str) -> None: ...
def drop_database(self, database: str) -> None: ...
@@ -7,7 +7,6 @@ path while phase-1 drains tenant DBs.
import atexit
import logging
import re
import threading
import time
from collections.abc import Iterator
@@ -187,13 +186,6 @@ class Neo4jSink(SinkDatabase):
return session.execute_read(_run)
# APOC virtual nodes report a bare negative number and virtual
# relationships a random UUID, neither in the stored-entity format.
_PERSISTED_ELEMENT_ID_RE = re.compile(r"^\d+:[^:]+:\d+$")
def is_persisted_element_id(self, element_id: str) -> bool:
return bool(self._PERSISTED_ELEMENT_ID_RE.match(element_id))
def create_database(self, database: str) -> None:
with self.get_session() as session:
session.run(
@@ -15,7 +15,6 @@ from api.attack_paths.queries.schema import (
RAW_SCHEMA_URL,
get_cartography_schema_query,
)
from api.attack_paths.sink.base import SinkDatabase
from api.models import AttackPathsScan
from config.custom_logging import BackendLogger
from config.env import env
@@ -136,7 +135,7 @@ def execute_query(
cypher = inject_provider_label(cypher, provider_id)
graph = backend.execute_read_query(database_name, cypher, parameters)
return _serialize_graph(graph, provider_id, backend)
return _serialize_graph(graph, provider_id)
except graph_database.WriteQueryNotAllowedException:
raise PermissionDenied(
@@ -175,8 +174,6 @@ def execute_custom_query(
# 1. `neo4j.READ_ACCESS` - prevents mutations at the driver level
# 2. `inject_provider_label()` - regex-based label injection scopes node patterns
# 3. `_serialize_graph()` - post-query filter drops nodes without the provider label
# and any node or relationship the backend did not store (APOC virtual
# entities can carry an arbitrary label and arbitrary properties)
# 4. `USING QUERY:TIMEOUTMILLISECONDS` on Neptune - server-side runaway cutoff
#
# Layer 2 is best-effort (regex can't fully parse Cypher);
@@ -196,7 +193,7 @@ def execute_custom_query(
try:
graph = backend.execute_read_query(database_name, cypher, None)
serialized = _serialize_graph(graph, provider_id, backend)
serialized = _serialize_graph(graph, provider_id)
return _truncate_graph(serialized)
except graph_database.ClientStatementException as exc:
@@ -268,15 +265,13 @@ def _truncate_graph(graph: dict[str, Any]) -> dict[str, Any]:
return graph
def _serialize_graph(graph, provider_id: str, backend: SinkDatabase) -> dict[str, Any]:
def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
provider_label = get_provider_label(provider_id)
nodes = []
kept_node_ids = set()
for node in graph.nodes:
if provider_label not in node.labels or not backend.is_persisted_element_id(
node.element_id
):
if provider_label not in node.labels:
continue
kept_node_ids.add(node.element_id)
@@ -291,7 +286,7 @@ def _serialize_graph(graph, provider_id: str, backend: SinkDatabase) -> dict[str
filtered_count = len(graph.nodes) - len(nodes)
if filtered_count > 0:
logger.debug(
f"Filtered {filtered_count} nodes without provider label {provider_label} or not stored"
f"Filtered {filtered_count} nodes without provider label {provider_label}"
)
relationships = []
@@ -299,7 +294,6 @@ def _serialize_graph(graph, provider_id: str, backend: SinkDatabase) -> dict[str
if (
relationship.start_node.element_id not in kept_node_ids
or relationship.end_node.element_id not in kept_node_ids
or not backend.is_persisted_element_id(relationship.element_id)
):
continue
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.49.0
version: 1.46.0
description: |-
Prowler API specification.
+201
View File
@@ -8,13 +8,20 @@ import api
import api.apps as api_apps_module
import pytest
from api.apps import (
ENCRYPTION_KEY_ENV,
ENCRYPTION_KEY_FILE,
PRIVATE_KEY_FILE,
PUBLIC_KEY_FILE,
SECRET_KEY_ENV,
SECRET_KEY_FILE,
SIGNING_KEY_ENV,
VERIFYING_KEY_ENV,
ApiConfig,
)
from cryptography.fernet import Fernet
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@pytest.fixture(autouse=True)
@@ -203,3 +210,197 @@ def test_ready_never_eagerly_initializes_neo4j_driver(monkeypatch, argv):
config.ready()
init_driver.assert_not_called()
@pytest.fixture
def secrets_dir(monkeypatch, tmp_path):
"""Isolated key directory with both symmetric secrets empty and generation enabled."""
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
monkeypatch.setattr(settings, "SECRET_KEY", settings.SECRET_KEY, raising=False)
monkeypatch.setattr(settings, "SECRETS_ENCRYPTION_KEY", "", raising=False)
monkeypatch.setattr(
settings, "DRF_API_KEY", settings.DRF_API_KEY.copy(), raising=False
)
monkeypatch.delenv(ENCRYPTION_KEY_ENV, raising=False)
monkeypatch.delenv(SECRET_KEY_ENV, raising=False)
return Path(tmp_path)
def _throwaway_fernet_key():
return Fernet.generate_key().decode()
def test_ensure_secrets_refuses_published_encryption_key(monkeypatch, secrets_dir):
published = _throwaway_fernet_key()
monkeypatch.setattr(settings, "SECRETS_ENCRYPTION_KEY", published, raising=False)
monkeypatch.setattr(
api_apps_module,
"PUBLISHED_ENCRYPTION_KEY_DIGESTS",
frozenset({api_apps_module._digest(published)}),
raising=False,
)
with pytest.raises(ImproperlyConfigured) as exc_info:
_make_app()._ensure_secrets()
assert ENCRYPTION_KEY_ENV in str(exc_info.value)
assert published not in str(exc_info.value)
assert not (secrets_dir / ENCRYPTION_KEY_FILE).exists()
def test_ensure_crypto_keys_refuses_published_signing_key(monkeypatch, tmp_path):
published, verifying = _stub_keys()
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.setattr(api_apps_module, "_keys_initialized", False, raising=False)
monkeypatch.setenv(SIGNING_KEY_ENV, published)
monkeypatch.setenv(VERIFYING_KEY_ENV, verifying)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
monkeypatch.setattr(
api_apps_module,
"PUBLISHED_SIGNING_KEY_DIGESTS",
frozenset({api_apps_module._pem_digest(published)}),
raising=False,
)
with pytest.raises(ImproperlyConfigured) as exc_info:
ApiConfig("api", api_apps_module)._ensure_crypto_keys()
assert SIGNING_KEY_ENV in str(exc_info.value)
assert "PRIVATE" not in str(exc_info.value)
assert not (Path(tmp_path) / PRIVATE_KEY_FILE).exists()
def test_pem_digest_ignores_indentation_and_wrapping():
# derived from the existing stub so no PEM literal is added to this file
flat = _stub_keys()[0]
indented = "".join(f" {line}\n" for line in flat.splitlines())
wrapped = flat.replace("PRIVATE\n", "PRIV\nATE\n", 1)
assert api_apps_module._pem_digest(indented) == api_apps_module._pem_digest(flat)
assert api_apps_module._pem_digest(wrapped) == api_apps_module._pem_digest(flat)
def test_ensure_secrets_generates_encryption_key_when_empty(secrets_dir):
_make_app()._ensure_secrets()
key_path = secrets_dir / ENCRYPTION_KEY_FILE
generated = key_path.read_text()
assert key_path.stat().st_mode & 0o777 == 0o600
Fernet(generated.encode())
assert settings.SECRETS_ENCRYPTION_KEY == generated
assert settings.DRF_API_KEY["FERNET_SECRET"] == generated
assert os.environ[ENCRYPTION_KEY_ENV] == generated
def test_ensure_secrets_reuses_persisted_encryption_key(monkeypatch, secrets_dir):
persisted = _throwaway_fernet_key()
(secrets_dir / ENCRYPTION_KEY_FILE).write_text(persisted + "\n")
def _fail_generate():
raise AssertionError("a persisted encryption key must not be regenerated")
monkeypatch.setattr(
ApiConfig, "_generate_encryption_key", staticmethod(_fail_generate)
)
_make_app()._ensure_secrets()
assert settings.SECRETS_ENCRYPTION_KEY == persisted
assert (secrets_dir / ENCRYPTION_KEY_FILE).read_text() == persisted + "\n"
def test_ensure_secrets_keeps_explicit_encryption_key(monkeypatch, secrets_dir):
explicit = _throwaway_fernet_key()
monkeypatch.setattr(settings, "SECRETS_ENCRYPTION_KEY", explicit, raising=False)
_make_app()._ensure_secrets()
assert settings.SECRETS_ENCRYPTION_KEY == explicit
assert not (secrets_dir / ENCRYPTION_KEY_FILE).exists()
@pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
def test_ensure_secrets_refuses_empty_encryption_key_when_unpersistable(
monkeypatch, secrets_dir
):
secrets_dir.chmod(0o500)
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", secrets_dir / "keys", raising=False
)
try:
with pytest.raises(ImproperlyConfigured) as exc_info:
_make_app()._ensure_secrets()
finally:
secrets_dir.chmod(0o700)
assert ENCRYPTION_KEY_ENV in str(exc_info.value)
assert settings.SECRETS_ENCRYPTION_KEY == ""
def test_ensure_secrets_recovers_when_another_process_wrote_the_key_first(
monkeypatch, secrets_dir
):
winner = _throwaway_fernet_key()
real_open = os.open
def _lose_the_race(path, flags, *args):
if Path(path).name == ENCRYPTION_KEY_FILE:
(secrets_dir / ENCRYPTION_KEY_FILE).write_text(winner)
return real_open(path, flags, *args)
monkeypatch.setattr(api_apps_module.os, "open", _lose_the_race)
_make_app()._ensure_secrets()
assert settings.SECRETS_ENCRYPTION_KEY == winner
def test_ensure_secrets_generates_secret_key_when_empty(monkeypatch, secrets_dir):
monkeypatch.setattr(
settings, "SECRETS_ENCRYPTION_KEY", _throwaway_fernet_key(), raising=False
)
_make_app()._ensure_secrets()
generated = (secrets_dir / SECRET_KEY_FILE).read_text()
assert len(generated) == 50
assert settings.SECRET_KEY == generated
assert os.environ[SECRET_KEY_ENV] == generated
def test_ensure_secrets_keeps_explicit_secret_key(monkeypatch, secrets_dir):
monkeypatch.setattr(
settings, "SECRETS_ENCRYPTION_KEY", _throwaway_fernet_key(), raising=False
)
monkeypatch.setenv(SECRET_KEY_ENV, "explicit-throwaway-secret-key")
_make_app()._ensure_secrets()
assert not (secrets_dir / SECRET_KEY_FILE).exists()
def test_ensure_secrets_is_skipped_while_testing(monkeypatch, secrets_dir):
monkeypatch.setattr(settings, "TESTING", True, raising=False)
_make_app()._ensure_secrets()
assert not (secrets_dir / ENCRYPTION_KEY_FILE).exists()
assert not (secrets_dir / SECRET_KEY_FILE).exists()
def test_import_models_settles_secrets_before_loading_models(monkeypatch):
calls = []
monkeypatch.setattr(
ApiConfig, "_ensure_secrets", lambda self: calls.append("secrets")
)
monkeypatch.setattr(AppConfig, "import_models", lambda self: calls.append("models"))
_make_app().import_models()
assert calls == ["secrets", "models"]
+1 -66
View File
@@ -321,7 +321,7 @@ def test_serialize_graph_filters_by_provider_label(attack_paths_graph_stub_class
relationships=[rel_keep, rel_drop_orphaned],
)
result = views_helpers._serialize_graph(graph, provider_id, _all_persisted())
result = views_helpers._serialize_graph(graph, provider_id)
assert len(result["nodes"]) == 1
assert result["nodes"][0]["id"] == "n1"
@@ -329,71 +329,6 @@ def test_serialize_graph_filters_by_provider_label(attack_paths_graph_stub_class
assert result["relationships"][0]["id"] == "r1"
def _all_persisted():
return SimpleNamespace(is_persisted_element_id=lambda element_id: True)
def test_serialize_graph_drops_virtual_entities(attack_paths_graph_stub_classes):
provider_id = "provider-keep"
plabel = get_provider_label(provider_id)
stored_ids = {"n1", "n2", "r1"}
backend = SimpleNamespace(
is_persisted_element_id=lambda element_id: element_id in stored_ids
)
node_1 = attack_paths_graph_stub_classes.Node("n1", ["AWSAccount", plabel], {})
node_2 = attack_paths_graph_stub_classes.Node("n2", ["AWSRole", plabel], {})
# apoc.create.vNode can carry the provider label and any payload
node_virtual = attack_paths_graph_stub_classes.Node(
"-1", ["AWSAccount", plabel], {"body": "fetched"}
)
rel_stored = attack_paths_graph_stub_classes.Relationship(
"r1", "OWNS", node_1, node_2, {}
)
# apoc.create.vRelationship between two stored nodes passes the endpoint check
rel_virtual = attack_paths_graph_stub_classes.Relationship(
"45cc10ea-9618-47fb-abaa-8e82a58d6b40", "V", node_1, node_2, {"body": "fetched"}
)
rel_to_virtual = attack_paths_graph_stub_classes.Relationship(
"r2", "OWNS", node_1, node_virtual, {}
)
graph = SimpleNamespace(
nodes=[node_1, node_2, node_virtual],
relationships=[rel_stored, rel_virtual, rel_to_virtual],
)
result = views_helpers._serialize_graph(graph, provider_id, backend)
assert [node["id"] for node in result["nodes"]] == ["n1", "n2"]
assert [rel["id"] for rel in result["relationships"]] == ["r1"]
assert "fetched" not in str(result)
@pytest.mark.parametrize(
"element_id, persisted",
[
("4:9f1d9d76-bb08-478c-b25d-7e577f967732:0", True),
("5:9f1d9d76-bb08-478c-b25d-7e577f967732:12", True),
("-2", False),
("45cc10ea-9618-47fb-abaa-8e82a58d6b40", False),
("", False),
],
ids=[
"stored_node",
"stored_relationship",
"virtual_node",
"virtual_relationship",
"empty",
],
)
def test_neo4j_sink_is_persisted_element_id(element_id, persisted):
from api.attack_paths.sink.neo4j import Neo4jSink
assert Neo4jSink().is_persisted_element_id(element_id) is persisted
# -- serialize_graph_as_text -------------------------------------------------------
@@ -222,12 +222,6 @@ class TestProtection:
result = _inject(cypher)
assert result.count(LABEL) == 1
def test_block_comment_with_node_pattern(self):
cypher = "MATCH (n:AWSRole) /* (m:Fake)\n(k) */ RETURN n"
result = _inject(cypher)
assert "/* (m:Fake)\n(k) */" in result
assert result.count(LABEL) == 1
# ---------------------------------------------------------------------------
# Clause splitting
@@ -445,35 +439,6 @@ class TestValidation:
assert "blocked operation" in str(exc.value.detail)
@pytest.mark.parametrize(
"cypher",
[
"LOAD /* c */ CSV FROM 'http://169.254.169.254/' AS x RETURN x",
"LOAD/**/CSV FROM 'http://evil.com' AS x RETURN x",
"LOAD /* a */ /* b */ CSV FROM 'http://evil.com' AS x RETURN x",
"LOAD /* multi\nline */ CSV FROM 'http://evil.com' AS x RETURN x",
"CALL apoc./**/load.json('http://evil.com/') YIELD value RETURN value",
"CALL apoc . load.json('http://evil.com/') YIELD value RETURN value",
"CALL apoc.load . json('http://evil.com/') YIELD value RETURN value",
"CALL apoc\n.cypher.run('CREATE (n)', {}) YIELD value RETURN value",
],
ids=[
"LOAD_CSV_block_comment",
"LOAD_CSV_empty_block_comment",
"LOAD_CSV_two_block_comments",
"LOAD_CSV_multiline_block_comment",
"apoc.load_block_comment",
"apoc.load_spaces_around_dot",
"apoc.load_spaces_before_procedure",
"apoc.cypher_newline_before_dot",
],
)
def test_rejects_blocked_patterns_split_by_comments_or_whitespace(self, cypher):
with pytest.raises(ValidationError) as exc:
validate_custom_query(cypher)
assert "blocked operation" in str(exc.value.detail)
@pytest.mark.parametrize(
"cypher",
[
@@ -483,8 +448,6 @@ class TestValidation:
"CALL apoc.create.vNode(['Label'], {}) YIELD node RETURN node",
"MATCH (n) WHERE n.name = 'apoc.load.json' RETURN n",
'MATCH (n) WHERE n.description = "LOAD CSV is cool" RETURN n',
"MATCH (n) /* LOAD CSV is documented here */ RETURN n",
"MATCH (n) WHERE n.note = '/* LOAD CSV */' RETURN n",
],
ids=[
"simple_match",
@@ -493,8 +456,6 @@ class TestValidation:
"apoc_virtual_node",
"apoc_load_inside_single_quotes",
"load_csv_inside_double_quotes",
"load_csv_inside_block_comment",
"block_comment_inside_string",
],
)
def test_allows_clean_queries(self, cypher):
-85
View File
@@ -12,7 +12,6 @@ from api.models import (
ProviderSecret,
Role,
RoleProviderGroupRelationship,
SAMLConfiguration,
Scan,
User,
UserRoleRelationship,
@@ -1897,90 +1896,6 @@ class TestUserRoleLinkPermissions:
assert response.status_code == status.HTTP_403_FORBIDDEN
SAML_METADATA_XML = """<?xml version='1.0' encoding='UTF-8'?>
<md:EntityDescriptor entityID='TEST' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'>
<md:IDPSSODescriptor WantAuthnRequestsSigned='false' protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol'>
<md:KeyDescriptor use='signing'>
<ds:KeyInfo xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
<ds:X509Data>
<ds:X509Certificate>TEST</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' Location='https://TEST/sso/saml'/>
<md:SingleSignOnService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' Location='https://TEST/sso/saml'/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
"""
@pytest.mark.django_db
class TestSAMLConfigurationPermissions:
def _create(self, client, email_domain):
return client.post(
reverse("saml-config-list"),
data={"email_domain": email_domain, "metadata_xml": SAML_METADATA_XML},
format="json",
)
def test_manage_integrations_only_cannot_create(
self, authenticated_client_rbac_manage_integrations_only
):
response = self._create(
authenticated_client_rbac_manage_integrations_only,
"integrations-only-create.com",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert not SAMLConfiguration.objects.filter(
email_domain="integrations-only-create.com"
).exists()
def test_manage_integrations_only_cannot_read_or_replace(
self, authenticated_client_rbac_manage_integrations_only
):
client = authenticated_client_rbac_manage_integrations_only
tenant = Membership.objects.filter(user=client.user).first().tenant
config = SAMLConfiguration.objects.create(
tenant_id=str(tenant.id),
email_domain="integrations-only-replace.com",
metadata_xml=SAML_METADATA_XML,
)
list_response = client.get(reverse("saml-config-list"))
patch_response = client.patch(
reverse("saml-config-detail", kwargs={"pk": config.id}),
data={"metadata_xml": SAML_METADATA_XML},
format="json",
)
delete_response = client.delete(
reverse("saml-config-detail", kwargs={"pk": config.id})
)
assert list_response.status_code == status.HTTP_403_FORBIDDEN
assert patch_response.status_code == status.HTTP_403_FORBIDDEN
assert delete_response.status_code == status.HTTP_403_FORBIDDEN
assert SAMLConfiguration.objects.filter(id=config.id).exists()
def test_manage_account_only_can_create_and_delete(
self, authenticated_client_rbac_manage_account
):
client = authenticated_client_rbac_manage_account
create_response = self._create(client, "account-only.com")
assert create_response.status_code == status.HTTP_201_CREATED
config = SAMLConfiguration.objects.get(email_domain="account-only.com")
delete_response = client.delete(
reverse("saml-config-detail", kwargs={"pk": config.id})
)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
assert not SAMLConfiguration.objects.filter(id=config.id).exists()
@pytest.mark.django_db
class TestCrossTenantRoleLeak:
"""Regression tests for get_role() cross-tenant privilege leak.
@@ -5,8 +5,6 @@ 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,
@@ -342,278 +340,6 @@ 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: ["}
+4 -96
View File
@@ -14,7 +14,6 @@ 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
@@ -36,34 +35,6 @@ 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):
@@ -317,40 +288,6 @@ 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(
@@ -468,6 +405,10 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.GOOGLEWORKSPACE.value,
{},
),
(
Provider.ProviderChoices.KUBERNETES.value,
{"context": "provider_uid"},
),
(
Provider.ProviderChoices.M365.value,
{},
@@ -518,39 +459,6 @@ 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,
):
-321
View File
@@ -63,7 +63,6 @@ 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,
@@ -98,11 +97,6 @@ from rest_framework_simplejwt.token_blacklist.models import (
OutstandingToken,
)
from rest_framework_simplejwt.tokens import RefreshToken
from tasks.jobs.backfill import (
aggregate_scan_category_summaries,
aggregate_scan_resource_group_summaries,
backfill_resource_scan_summaries,
)
class TestViewSet:
@@ -139,145 +133,6 @@ 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",
@@ -7094,99 +6949,6 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_400_BAD_REQUEST
def _client_restricted_to_provider(tenant, provider, factory):
"""Authenticated client for a fresh member whose role only sees `provider`."""
user = User.objects.create_user(
name=f"restricted-{uuid4()}",
email=f"restricted-{uuid4()}@prowler.com",
password=TEST_PASSWORD,
)
Membership.objects.create(
user=user, tenant=tenant, role=Membership.RoleChoices.MEMBER
)
role = Role.objects.create(
name=f"restricted-{uuid4()}",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant.id)
provider_group = ProviderGroup.objects.create(
name=f"restricted-{uuid4()}", tenant_id=tenant.id
)
ProviderGroupMembership.objects.create(
tenant_id=tenant.id, provider_group=provider_group, provider=provider
)
RoleProviderGroupRelationship.objects.create(
tenant_id=tenant.id, role=role, provider_group=provider_group
)
return factory(user, tenant)
def _completed_scan_with_summaries(
tenant, provider, resources, *, categories, resource_group
):
"""Completed scan for `provider` with one finding over `resources`, summary tables filled."""
tenant_id = str(tenant.id)
scan = Scan.objects.create(
name=f"scan-{uuid4()}",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant_id,
)
finding = Finding.objects.create(
tenant_id=tenant_id,
uid=f"finding-{uuid4()}",
scan=scan,
delta="new",
status=Status.FAIL,
status_extended="test status",
impact=Severity.critical,
impact_extended="test impact",
severity=Severity.critical,
raw_result={"status": Status.FAIL},
check_id="scoped_check",
check_metadata={"CheckId": "scoped_check"},
categories=categories,
resource_groups=resource_group,
first_seen_at="2024-01-02T00:00:00Z",
)
finding.add_resources(resources)
backfill_resource_scan_summaries(tenant_id, str(scan.id))
aggregate_scan_category_summaries(tenant_id, str(scan.id))
aggregate_scan_resource_group_summaries(tenant_id, str(scan.id))
return scan
@pytest.fixture
def scans_per_provider_fixture(tenants_fixture, aws_provider_pair, resources_fixture):
"""One completed scan per provider; provider 2 only owns the `test` typed resource."""
tenant = tenants_fixture[0]
provider1, provider2 = aws_provider_pair
resource1, resource2, resource3 = resources_fixture
_completed_scan_with_summaries(
tenant,
provider1,
[resource1, resource2],
categories=["iam"],
resource_group="identity",
)
_completed_scan_with_summaries(
tenant,
provider2,
[resource3],
categories=["data-protection"],
resource_group="data",
)
return tenant, provider2
@pytest.mark.django_db
class TestResourceViewSet:
def test_resources_list_none(self, authenticated_client):
@@ -7558,42 +7320,6 @@ class TestResourceViewSet:
assert attributes["types"] == [latest_scan_resource.type]
assert "groups" in attributes
def test_resources_metadata_restricted_to_provider_group(
self, authenticated_client_for_tenant_factory, scans_per_provider_fixture
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(
reverse("resource-metadata"), {"filter[updated_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["types"]) == {"test"}
assert set(attributes["groups"]) == {"compute"}
def test_resources_metadata_latest_restricted_to_provider_group(
self, authenticated_client_for_tenant_factory, scans_per_provider_fixture
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(reverse("resource-metadata_latest"))
assert response.status_code == status.HTTP_200_OK
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["types"]) == {"test"}
assert set(attributes["groups"]) == {"compute"}
def test_resources_latest_filter_by_provider_id(
self, authenticated_client, latest_scan_resource
):
@@ -9226,53 +8952,6 @@ class TestFindingViewSet:
assert "groups" in attributes
assert "ai_ml" in attributes["groups"]
@patch("api.v1.views.backfill_scan_resource_summaries_task.apply_async")
def test_findings_metadata_restricted_to_provider_group(
self,
mock_backfill,
authenticated_client_for_tenant_factory,
scans_per_provider_fixture,
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(
reverse("finding-metadata"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
mock_backfill.assert_not_called()
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["resource_types"]) == {"test"}
assert set(attributes["categories"]) == {"data-protection"}
@patch("api.v1.views.backfill_scan_resource_summaries_task.apply_async")
def test_findings_metadata_latest_restricted_to_provider_group(
self,
mock_backfill,
authenticated_client_for_tenant_factory,
scans_per_provider_fixture,
):
tenant, provider2 = scans_per_provider_fixture
client = _client_restricted_to_provider(
tenant, provider2, authenticated_client_for_tenant_factory
)
response = client.get(reverse("finding-metadata_latest"))
assert response.status_code == status.HTTP_200_OK
mock_backfill.assert_not_called()
attributes = response.json()["data"]["attributes"]
assert set(attributes["services"]) == {"ec2"}
assert set(attributes["regions"]) == {"us-east-1"}
assert set(attributes["resource_types"]) == {"test"}
assert set(attributes["categories"]) == {"data-protection"}
assert set(attributes["groups"]) == {"data"}
def test_findings_filter_by_category(
self, authenticated_client, findings_with_categories
):
+1 -19
View File
@@ -17,7 +17,7 @@ from api.models import (
Role,
UserRoleRelationship,
)
from api.v1.serializers import FindingMetadataSerializer, KubernetesProviderSecret
from api.v1.serializers import FindingMetadataSerializer
from django.contrib.postgres.aggregates import ArrayAgg
from django.db import transaction
from django.db.models import Subquery
@@ -201,13 +201,6 @@ 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:
@@ -232,7 +225,6 @@ 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:
@@ -400,16 +392,6 @@ 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,8 +214,7 @@ from rest_framework_json_api import serializers
"kubeconfig_content": {
"type": "string",
"description": "The content of the Kubernetes kubeconfig file, encoded as a string. "
"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.",
"Kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.",
}
},
"required": ["kubeconfig_content"],
+34 -109
View File
@@ -377,48 +377,27 @@ 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")
return bool(
is_self = bool(
request
and getattr(request, "user", None)
and getattr(instance, "id", None) == request.user.id
)
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)
]
return is_self or (role and role.manage_account)
def get_roles(self, instance):
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)
return (
instance.roles.all()
if self._can_view_relationships(instance)
else Role.objects.none()
)
def get_memberships(self, instance):
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)
return (
instance.memberships.all()
if self._can_view_relationships(instance)
else Membership.objects.none()
)
class UserMeSerializer(UserSerializer):
@@ -752,32 +731,11 @@ 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):
"""Resolve the tenant only when the requester is a member of it."""
return self._requester_tenants().get(instance.tenant_id)
try:
return Tenant.objects.using(MainRouter.admin_db).get(id=instance.tenant_id)
except Tenant.DoesNotExist:
return None
# Provider Groups
@@ -1701,43 +1659,14 @@ class FindingMetadataSerializer(BaseSerializerV1):
# Provider secrets
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_UNSUPPORTED_COMMAND_AUTH_ERROR = (
"Kubernetes kubeconfig command-based authentication is not supported in "
"Prowler Cloud for security reasons."
)
KUBERNETES_KUBECONFIG_INVALID_ERROR = "Invalid Kubernetes kubeconfig content."
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."""
def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool:
users = kubeconfig.get("users", [])
if not isinstance(users, list):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
@@ -1750,25 +1679,21 @@ def kubeconfig_is_inline_only_credentials(kubeconfig: dict) -> bool:
if not isinstance(user, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if any(key not in KUBECONFIG_ALLOWED_USER_KEYS for key in user):
return False
if "exec" in user:
return True
clusters = kubeconfig.get("clusters", [])
if not isinstance(clusters, list):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
auth_provider = user.get("auth-provider", {})
if not isinstance(auth_provider, dict):
continue
for cluster_entry in clusters:
if not isinstance(cluster_entry, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
auth_provider_config = auth_provider.get("config", {})
if not isinstance(auth_provider_config, dict):
continue
cluster = cluster_entry.get("cluster", {})
if not isinstance(cluster, dict):
raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if "cmd-path" in auth_provider_config:
return True
if any(key not in KUBECONFIG_ALLOWED_CLUSTER_KEYS for key in cluster):
return False
return True
return False
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
@@ -1951,7 +1876,7 @@ class MongoDBAtlasProviderSecret(serializers.Serializer):
class KubernetesProviderSecret(serializers.Serializer):
kubeconfig_content = serializers.CharField(trim_whitespace=False)
kubeconfig_content = serializers.CharField()
def validate_kubeconfig_content(self, kubeconfig_content):
try:
@@ -1964,9 +1889,9 @@ class KubernetesProviderSecret(serializers.Serializer):
if not isinstance(kubeconfig, dict):
raise serializers.ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR)
if not kubeconfig_is_inline_only_credentials(kubeconfig):
if kubeconfig_contains_unsupported_command_auth(kubeconfig):
raise serializers.ValidationError(
KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR
)
return kubeconfig_content
+16 -55
View File
@@ -351,15 +351,6 @@ from tasks.tasks import (
logger = logging.getLogger(BackendLogger.API)
def _scans_visible_to(role, tenant_id):
"""Tenant scans, narrowed to the role's provider groups unless it has unlimited visibility."""
scans = Scan.all_objects.filter(tenant_id=tenant_id)
if role.unlimited_visibility:
return scans
return scans.filter(provider__in=get_providers(role))
CACHE_DECORATOR = cache_control(
max_age=django_settings.CACHE_MAX_AGE,
stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE,
@@ -808,9 +799,7 @@ class SAMLConfigurationViewSet(BaseRLSViewSet):
"""
serializer_class = SAMLConfigurationSerializer
# The IdP behind this configuration decides who signs in and which role
# they get, so writing it is account administration, not an integration.
required_permissions = [Permissions.MANAGE_ACCOUNT]
required_permissions = [Permissions.MANAGE_INTEGRATIONS]
queryset = SAMLConfiguration.objects.all()
def get_queryset(self):
@@ -1907,7 +1896,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = self.user_role
user_roles = get_role(self.request.user, self.request.tenant_id)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all providers
queryset = Provider.objects.filter(tenant_id=self.request.tenant_id)
@@ -3069,7 +3058,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def get_queryset(self):
user_roles = self.user_role
user_roles = get_role(self.request.user, self.request.tenant_id)
base_queryset = AttackPathsScan.objects.filter(tenant_id=self.request.tenant_id)
if user_roles.unlimited_visibility:
@@ -3423,7 +3412,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
required_permissions = []
def get_queryset(self):
user_roles = self.user_role
user_roles = get_role(self.request.user, self.request.tenant_id)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all scans
queryset = Resource.all_objects.filter(tenant_id=self.request.tenant_id)
@@ -3555,15 +3544,8 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = self.user_role
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
if not user_roles.unlimited_visibility:
queryset = queryset.filter(
scan_id__in=_scans_visible_to(user_roles, tenant_id).values_list(
"id", flat=True
)
)
if scans := query_params.get("filter[scan__in]") or query_params.get(
"filter[scan]"
@@ -3641,10 +3623,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
)
if not user_roles.unlimited_visibility:
all_groups = all_groups.filter(provider__in=get_providers(user_roles))
all_groups = all_groups.values_list("groups", flat=True)
).values_list("groups", flat=True)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -3669,10 +3648,9 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
def metadata_latest(self, request):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = self.user_role
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
latest_scans_queryset = Scan.all_objects.filter(
tenant_id=tenant_id
).latest_per_provider()
queryset = ResourceScanSummary.objects.filter(
@@ -3713,10 +3691,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
)
if not user_roles.unlimited_visibility:
all_groups = all_groups.filter(provider__in=get_providers(user_roles))
all_groups = all_groups.values_list("groups", flat=True)
).values_list("groups", flat=True)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -4053,7 +4028,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
def get_queryset(self):
tenant_id = self.request.tenant_id
user_roles = self.user_role
user_roles = get_role(self.request.user, self.request.tenant_id)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all findings
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
@@ -4127,16 +4102,8 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = self.user_role
visible_scan_ids = (
None
if user_roles.unlimited_visibility
else _scans_visible_to(user_roles, tenant_id).values_list("id", flat=True)
)
queryset = ResourceScanSummary.objects.filter(tenant_id=tenant_id)
if not user_roles.unlimited_visibility:
queryset = queryset.filter(scan_id__in=visible_scan_ids)
scan_based_filters = {}
category_scan_filters = {} # Filters for ScanCategorySummary
@@ -4194,10 +4161,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
if not queryset.exists():
raw_scans_ids = Scan.objects.filter(
tenant_id=tenant_id, **scan_based_filters
)
if not user_roles.unlimited_visibility:
raw_scans_ids = raw_scans_ids.filter(id__in=visible_scan_ids)
raw_scans_ids = raw_scans_ids.values_list("id", "unique_resource_count")
).values_list("id", "unique_resource_count")
scan_ids = [
scan_id for scan_id, count in raw_scans_ids if count and count > 0
]
@@ -4239,13 +4203,11 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
)
# Get categories from ScanCategorySummary using same scan filters
category_summaries = ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
if not user_roles.unlimited_visibility:
category_summaries = category_summaries.filter(scan_id__in=visible_scan_ids)
categories = list(
category_summaries.values_list("category", flat=True)
ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
.values_list("category", flat=True)
.distinct()
.order_by("category")
)
@@ -4300,10 +4262,9 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
def metadata_latest(self, request):
tenant_id = request.tenant_id
query_params = request.query_params
user_roles = self.user_role
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
latest_scans_queryset = Scan.all_objects.filter(
tenant_id=tenant_id
).latest_per_provider()
raw_latest_scans_ids = list(
latest_scans_queryset.values_list("id", "unique_resource_count")
+1 -3
View File
@@ -9,7 +9,7 @@ from config.settings.sentry import * # noqa
from config.settings.social_login import * # noqa
from django.core.exceptions import ImproperlyConfigured
SECRET_KEY = env("SECRET_KEY", default="secret")
SECRET_KEY = env("SECRET_KEY", default="")
DEBUG = env.bool("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
@@ -130,8 +130,6 @@ REST_FRAMEWORK = {
SPECTACULAR_SETTINGS = {
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
# redoc@latest is 3.x, whose standalone bundle fails as a classic script
"REDOC_DIST": "https://cdn.jsdelivr.net/npm/redoc@2.5.1",
"PREPROCESSING_HOOKS": [
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
],
+2
View File
@@ -18,6 +18,8 @@ DATABASES = {
DATABASE_ROUTERS = []
TESTING = True
# `_ensure_secrets` is skipped while testing, and Django rejects an empty SECRET_KEY
SECRET_KEY = env("SECRET_KEY", default="insecure-testing-secret-key-do-not-use-in-prod")
# Override page size for testing to a value only slightly above the current fixture count.
# We explicitly set PAGE_SIZE to 15 (round number just above fixture) to avoid masking pagination bugs, while not setting it excessively high.
# If you add more providers to the fixture, please review that the total value is below the current one and update this value if needed.
-4
View File
@@ -53,10 +53,6 @@ preload_app = not DEBUG
# that may take longer, such as complex API operations.
timeout = env.int("GUNICORN_TIMEOUT", default=120)
# Recycle workers after N requests (+ jitter) to cap memory growth. GUNICORN_MAX_REQUESTS=0 disables.
max_requests = env.int("GUNICORN_MAX_REQUESTS", default=10000)
max_requests_jitter = env.int("GUNICORN_MAX_REQUESTS_JITTER", default=1000)
# Logging
logconfig_dict = DJANGO_LOGGERS
gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)
-41
View File
@@ -443,47 +443,6 @@ def authenticated_client_rbac_manage_users_only(
)
@pytest.fixture(scope="function")
def create_test_user_rbac_manage_integrations_only(django_db_setup, django_db_blocker):
"""User with only manage_integrations permission (no manage_account)."""
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing_manage_integrations_only",
email="rbac_manage_integrations_only@rbac.com",
password=TEST_PASSWORD,
)
tenant = Tenant.objects.create(name="Tenant Test Manage Integrations Only")
Membership.objects.create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.OWNER,
)
role = Role.objects.create(
name="manage_integrations_only",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=True,
manage_scans=False,
unlimited_visibility=False,
)
UserRoleRelationship.objects.create(user=user, role=role, tenant_id=tenant.id)
return user
@pytest.fixture
def authenticated_client_rbac_manage_integrations_only(
create_test_user_rbac_manage_integrations_only, client
):
return authenticate_client_for_tenant(
client,
create_test_user_rbac_manage_integrations_only,
first_membership_tenant(create_test_user_rbac_manage_integrations_only),
)
@pytest.fixture
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
return authenticate_client_for_tenant(
+8 -4
View File
@@ -172,10 +172,14 @@ class CISReportGenerator(BaseComplianceReportGenerator):
# Metadata table via base class helper
info_rows = self._build_info_rows(data, language=self.config.language)
metadata_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
metadata_data = []
for label, value in info_rows:
if label in ("Name:", "Description:") and value:
metadata_data.append(
[label, Paragraph(str(value), self.styles["normal_center"])]
)
else:
metadata_data.append([label, value])
metadata_table = Table(metadata_data, colWidths=[2 * inch, 4 * inch])
metadata_table.setStyle(
+10 -5
View File
@@ -13,7 +13,7 @@ from .base import (
get_requirement_metadata,
)
from .charts import create_horizontal_bar_chart, create_radar_chart
from .components import escape_html, get_color_for_compliance
from .components import get_color_for_compliance
from .config import (
COLOR_BG_BLUE,
COLOR_BLUE,
@@ -99,10 +99,15 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
# Compliance info table - use base class helper for consistency
info_rows = self._build_info_rows(data, language="es")
info_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
# Convert tuples to lists and wrap long text in Paragraphs
info_data = []
for label, value in info_rows:
if label in ("Nombre:", "Descripción:") and value:
info_data.append(
[label, Paragraph(value, self.styles["normal_center"])]
)
else:
info_data.append([label, value])
info_table = Table(info_data, colWidths=[2 * inch, 4 * inch])
info_table.setStyle(
+9 -5
View File
@@ -11,7 +11,6 @@ from .base import (
get_requirement_metadata,
)
from .charts import create_horizontal_bar_chart, get_chart_color_for_percentage
from .components import escape_html
from .config import (
COLOR_BORDER_GRAY,
COLOR_DARK_GRAY,
@@ -112,10 +111,15 @@ class NIS2ReportGenerator(BaseComplianceReportGenerator):
# Compliance metadata table - use base class helper for consistency
info_rows = self._build_info_rows(data, language="en")
metadata_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
# Convert tuples to lists and wrap long text in Paragraphs
metadata_data = []
for label, value in info_rows:
if label in ("Name:", "Description:") and value:
metadata_data.append(
[label, Paragraph(value, self.styles["normal_center"])]
)
else:
metadata_data.append([label, value])
metadata_table = Table(metadata_data, colWidths=[2 * inch, 4 * inch])
metadata_table.setStyle(
+4 -5
View File
@@ -997,12 +997,11 @@ def _process_finding_micro_batch(
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.type = finding.resource_type
updated = True
serialized_metadata = json.dumps(
finding.resource_metadata, cls=CustomEncoder
)
if resource_instance.metadata != serialized_metadata:
if resource_instance.metadata != finding.resource_metadata:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.metadata = serialized_metadata
resource_instance.metadata = json.dumps(
finding.resource_metadata, cls=CustomEncoder
)
updated = True
if resource_instance.details != finding.resource_details:
snapshot_resource_fields(resource_uid, resource_instance)
+1 -7
View File
@@ -897,12 +897,8 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
)
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
for batch, is_last in batched(qs, DJANGO_FINDINGS_BATCH_SIZE):
metadata_cache = {}
fos = [
FindingOutput.transform_api_finding(
f, prowler_provider, metadata_cache=metadata_cache
)
for f in batch
FindingOutput.transform_api_finding(f, prowler_provider) for f in batch
]
# Outputs
@@ -983,8 +979,6 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
writer.batch_write_data_to_file()
writer._data.clear()
del fos, metadata_cache
compressed = _compress_output_files(out_dir)
upload_uri = _upload_to_s3(
+3 -39
View File
@@ -3,8 +3,7 @@ from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
from api.models import Integration, LighthouseConfiguration, Provider, ProviderSecret
from api.v1.serializers import KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR
from api.models import Integration, LighthouseConfiguration, Provider
from tasks.jobs.connection import (
check_integration_connection,
check_lighthouse_connection,
@@ -66,52 +65,17 @@ 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(
"Unable to assume role"
)
mock_provider_connection_test.return_value.error = Exception()
result = check_provider_connection(provider_id="provider_id")
assert result["connected"] is False
assert result["error"] == "Unable to assume role"
assert result["error"] is not None
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",
[
@@ -2,7 +2,6 @@ from unittest.mock import Mock, patch
import pytest
from api.models import StatusChoices
from reportlab.lib.units import inch
from reportlab.platypus import Image, LongTable, Paragraph, Table
from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData
from tasks.jobs.reports import cis as cis_report_module
@@ -583,24 +582,3 @@ class TestCISCoverPage:
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "AWS" in content
def test_cover_page_wraps_long_account_id(
self, cis_generator, basic_cis_compliance_data
):
provider = Mock()
provider.provider = "oraclecloud"
provider.uid = "ocid1.tenancy.oc1.." + "a" * 70
provider.alias = None
basic_cis_compliance_data.provider_obj = provider
elements = cis_generator.create_cover_page(basic_cis_compliance_data)
table = next(
e
for e in elements
if isinstance(e, Table)
and any(row[0] == "Account ID:" for row in e._cellvalues)
)
values = [row[1] for row in table._cellvalues]
assert all(isinstance(v, Paragraph) for v in values)
account_id = next(v for v in values if v.text == provider.uid)
_, height = account_id.wrap(4 * inch, 1000)
assert height > account_id.style.leading
@@ -2,7 +2,6 @@ import io
from unittest.mock import Mock, patch
import pytest
from reportlab.lib.units import inch
from reportlab.platypus import Image, PageBreak, Paragraph, Table
from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData
from tasks.jobs.reports import ens as ens_report_module
@@ -190,31 +189,6 @@ class TestENSCoverPage:
tables = [e for e in elements if isinstance(e, Table)]
assert len(tables) >= 1 # At least info table
def test_cover_page_wraps_long_account_id(
self, ens_generator, basic_ens_compliance_data
):
provider = Mock()
provider.provider = "oraclecloud"
provider.uid = "ocid1.tenancy.oc1.." + "a" * 70
provider.alias = None
basic_ens_compliance_data.provider_obj = provider
basic_ens_compliance_data.requirements = []
basic_ens_compliance_data.attributes_by_requirement_id = {}
elements = ens_generator.create_cover_page(basic_ens_compliance_data)
table = next(
e
for e in elements
if isinstance(e, Table)
and any(row[0] == "Account ID:" for row in e._cellvalues)
)
values = [row[1] for row in table._cellvalues]
assert all(isinstance(v, Paragraph) for v in values)
account_id = next(v for v in values if v.text == provider.uid)
_, height = account_id.wrap(4 * inch, 1000)
assert height > account_id.style.leading
def test_cover_page_has_warning_about_manual(
self, ens_generator, basic_ens_compliance_data
):
@@ -2,7 +2,6 @@ import io
from unittest.mock import Mock, patch
import pytest
from reportlab.lib.units import inch
from reportlab.platypus import Image, PageBreak, Paragraph, Table
from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData
from tasks.jobs.reports import nis2 as nis2_report_module
@@ -215,31 +214,6 @@ class TestNIS2CoverPage:
tables = [e for e in elements if isinstance(e, Table)]
assert len(tables) >= 1
def test_cover_page_wraps_long_account_id(
self, nis2_generator, basic_nis2_compliance_data
):
provider = Mock()
provider.provider = "oraclecloud"
provider.uid = "ocid1.tenancy.oc1.." + "a" * 70
provider.alias = None
basic_nis2_compliance_data.provider_obj = provider
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_cover_page(basic_nis2_compliance_data)
table = next(
e
for e in elements
if isinstance(e, Table)
and any(row[0] == "Account ID:" for row in e._cellvalues)
)
values = [row[1] for row in table._cellvalues]
assert all(isinstance(v, Paragraph) for v in values)
account_id = next(v for v in values if v.text == provider.uid)
_, height = account_id.wrap(4 * inch, 1000)
assert height > account_id.style.leading
# =============================================================================
# Executive Summary Tests
-72
View File
@@ -2349,78 +2349,6 @@ class TestProcessFindingMicroBatch:
assert resource_cache[finding.resource_uid].service == finding.service_name
assert tag_cache.keys() == {("team", "devsec")}
def test_process_finding_micro_batch_skips_unchanged_resource(
self, tenants_fixture, scans_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = scan.provider
resource_metadata = {"owner": "platform", "ports": [22, 443]}
existing_resource = Resource.objects.create(
tenant_id=tenant.id,
provider=provider,
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-001",
name="i-001",
region="us-east-1",
service="ec2",
type="instance",
metadata=json.dumps(resource_metadata, cls=CustomEncoder),
details="details",
partition="aws",
)
finding = FakeFinding(
uid="finding-unchanged",
status=StatusChoices.PASS,
status_extended="passing",
severity=Severity.low,
check_id="ec2_public_instance",
resource_uid=existing_resource.uid,
resource_name=existing_resource.name,
region=existing_resource.region,
service_name=existing_resource.service,
resource_type=existing_resource.type,
resource_tags={},
resource_metadata=resource_metadata,
resource_details=existing_resource.details,
partition=existing_resource.partition,
raw={"status": "PASS"},
compliance={},
metadata={"source": "prowler"},
muted=False,
)
with (
patch("tasks.jobs.scan.rls_transaction", new=noop_rls_transaction),
patch("api.db_utils.rls_transaction", new=noop_rls_transaction),
patch(
"tasks.jobs.scan.Resource.objects.bulk_update",
wraps=Resource.objects.bulk_update,
) as bulk_update,
):
_process_finding_micro_batch(
str(tenant.id),
[finding],
scan,
provider,
{existing_resource.uid: existing_resource},
{},
{},
{existing_resource.uid: 0},
set(),
set(),
{},
{},
{},
{},
)
bulk_update.assert_not_called()
assert Finding.objects.filter(
tenant_id=tenant.id, scan_id=scan.id, uid=finding.uid
).exists()
def test_process_finding_micro_batch_refreshes_empty_resource_name(
self, tenants_fixture, scans_fixture
):
+1 -1
View File
@@ -742,7 +742,7 @@ class TestGenerateOutputs:
patch("tasks.tasks.FindingOutput._transform_findings_stats"),
patch(
"tasks.tasks.FindingOutput.transform_api_finding",
side_effect=lambda f, _prov, **_kw: f,
side_effect=lambda f, _prov: f,
),
patch("tasks.tasks._compress_output_files", return_value="outdir.zip"),
patch("tasks.tasks._upload_to_s3", return_value="s3://bucket/outdir.zip"),
+1 -17
View File
@@ -1,11 +1,10 @@
import json
from datetime import UTC, datetime, timedelta
from unittest.mock import patch
import pytest
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from django_celery_results.models import TaskResult
from tasks.utils import CustomEncoder, batched, get_next_execution_datetime
from tasks.utils import batched, get_next_execution_datetime
@pytest.mark.django_db
@@ -101,18 +100,3 @@ class TestBatchedFunction:
result = list(batched([1, 2, 3], 5))
expected = [([1, 2, 3], True)]
assert result == expected
class TestCustomEncoder:
def test_set_serialized_in_stable_order(self):
zones = {"us-east-1d", "us-east-1a", "us-east-1c", "us-east-1b"}
assert json.dumps({"zones": zones}, cls=CustomEncoder) == json.dumps(
{"zones": "{'us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d'}"}
)
def test_empty_set_keeps_str_representation(self):
assert json.dumps(set(), cls=CustomEncoder) == json.dumps("set()")
def test_frozenset_serialized_like_set(self):
assert json.dumps(frozenset({2, 1}), cls=CustomEncoder) == json.dumps("{1, 2}")
-5
View File
@@ -19,11 +19,6 @@ class CustomEncoder(json.JSONEncoder):
return o.isoformat(timespec="seconds")
if isinstance(o, timedelta):
return o.total_seconds()
# Same representation as str(set), with a stable order across processes
if isinstance(o, (set, frozenset)):
if not o:
return "set()"
return "{" + ", ".join(repr(item) for item in sorted(o, key=repr)) + "}"
# Custom object serialization
try:
Generated
+148 -156
View File
@@ -16,7 +16,7 @@ constraints = [
{ name = "aiobotocore", specifier = "==2.25.1" },
{ name = "aiofiles", specifier = "==24.1.0" },
{ name = "aiohappyeyeballs", specifier = "==2.6.1" },
{ name = "aiohttp", specifier = "==3.14.5" },
{ name = "aiohttp", specifier = "==3.14.3" },
{ name = "aioitertools", specifier = "==0.13.0" },
{ name = "aiosignal", specifier = "==1.4.0" },
{ name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" },
@@ -112,7 +112,7 @@ constraints = [
{ name = "botocore", specifier = "==1.40.61" },
{ name = "cartography", specifier = "==0.138.1" },
{ name = "celery", specifier = "==5.6.2" },
{ name = "certifi", specifier = "==2026.10.19" },
{ name = "certifi", specifier = "==2026.1.4" },
{ name = "cffi", specifier = "==2.0.0" },
{ name = "charset-normalizer", specifier = "==3.4.4" },
{ name = "circuitbreaker", specifier = "==2.1.3" },
@@ -139,7 +139,7 @@ constraints = [
{ name = "distro", specifier = "==1.9.0" },
{ name = "dj-rest-auth", specifier = "==7.0.1" },
{ name = "django", specifier = "==5.2.17" },
{ name = "django-allauth", specifier = "==65.19.7" },
{ name = "django-allauth", specifier = "==65.15.0" },
{ name = "django-celery-beat", specifier = "==2.9.0" },
{ name = "django-celery-results", specifier = "==2.6.0" },
{ name = "django-cors-headers", specifier = "==4.4.0" },
@@ -150,10 +150,10 @@ constraints = [
{ name = "django-postgres-extra", specifier = "==2.0.9" },
{ name = "django-silk", specifier = "==5.3.2" },
{ name = "django-timezone-field", specifier = "==7.2.1" },
{ name = "djangorestframework", specifier = "==3.18.3" },
{ name = "djangorestframework", specifier = "==3.15.2" },
{ name = "djangorestframework-jsonapi", specifier = "==7.0.2" },
{ name = "djangorestframework-simplejwt", specifier = "==5.5.1" },
{ name = "dnspython", specifier = "==2.9.0" },
{ name = "dnspython", specifier = "==2.8.0" },
{ name = "docker", specifier = "==7.1.0" },
{ name = "dogpile-cache", specifier = "==1.5.0" },
{ name = "dparse", specifier = "==0.6.4" },
@@ -253,7 +253,7 @@ constraints = [
{ name = "nest-asyncio", specifier = "==1.6.0" },
{ name = "nltk", specifier = "==3.9.4" },
{ name = "numpy", specifier = "==2.2.6" },
{ name = "oauthlib", specifier = "==4.0.0" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "oci", specifier = "==2.184.1" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "openstacksdk", specifier = "==4.2.0" },
@@ -266,7 +266,7 @@ constraints = [
{ name = "pagerduty", specifier = "==6.1.0" },
{ name = "pandas", specifier = "==2.2.3" },
{ name = "pbr", specifier = "==7.0.3" },
{ name = "pillow", specifier = "==13.0.0" },
{ name = "pillow", specifier = "==12.3.0" },
{ name = "pkginfo", specifier = "==1.12.1.2" },
{ name = "platformdirs", specifier = "==4.5.1" },
{ name = "plotly", specifier = "==6.5.2" },
@@ -369,7 +369,7 @@ constraints = [
{ name = "xlsxwriter", specifier = "==3.2.9" },
{ name = "xmlsec", specifier = "==1.3.17" },
{ name = "xmltodict", specifier = "==1.0.2" },
{ name = "yarl", specifier = "==1.25.1" },
{ name = "yarl", specifier = "==1.22.0" },
{ name = "zipp", specifier = "==3.23.0" },
{ name = "zope-event", specifier = "==6.1" },
{ name = "zope-interface", specifier = "==8.2" },
@@ -388,7 +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.3" },
{ name = "pymongo", specifier = "==4.18.2" },
]
[[package]]
@@ -480,7 +480,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.14.5"
version = "3.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -492,45 +492,44 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/4c/bdccd81e9ee225b69c60e7766c9a5b05364f118f4d383713b89a682d772d/aiohttp-3.14.5.tar.gz", hash = "sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178", size = 8078052, upload-time = "2026-10-11T01:05:12.408Z" }
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/f3/8997f18890a92c79f77fcfbb4f78ca17c2d4ab109e9eb6d31b4e9de194a0/aiohttp-3.14.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f", size = 832358, upload-time = "2026-10-11T00:59:42.195Z" },
{ url = "https://files.pythonhosted.org/packages/8e/42/084651e9efb5cadb99265f786df60f2a7b353cead7bfd2c87003cb4867ad/aiohttp-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a", size = 559636, upload-time = "2026-10-11T00:59:43.677Z" },
{ url = "https://files.pythonhosted.org/packages/3d/fe/92838944e601f0fe195fd3f3ada37e29fbee3d19bfd323fe15937ec79d36/aiohttp-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593", size = 552588, upload-time = "2026-10-11T00:59:45.592Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b8/dd9b95c5c20ceae1b47738e656bd79e9883e3117a9cc8b143901eb604e9a/aiohttp-3.14.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4", size = 1928224, upload-time = "2026-10-11T00:59:47.294Z" },
{ url = "https://files.pythonhosted.org/packages/c6/47/70010cd2ba8746968d53d433ff328c33f06a66c99da5f3de759c64a6b2eb/aiohttp-3.14.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f", size = 1877674, upload-time = "2026-10-11T00:59:49.472Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a1/b026f071dbd87f0bdba86b92d47e46f3899e3ad7143b6e0fe7e7887090b4/aiohttp-3.14.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12", size = 1985850, upload-time = "2026-10-11T00:59:51.296Z" },
{ url = "https://files.pythonhosted.org/packages/36/d1/f7b6f6f8c3cb5a71b53baeddc2e70c224c28b12a23a55c42994c504019b7/aiohttp-3.14.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939", size = 2105471, upload-time = "2026-10-11T00:59:53.191Z" },
{ url = "https://files.pythonhosted.org/packages/42/74/2a2b22953de15c6c8a80debf26db3047e4e8a5a5db6dd7c2c6c01eec0605/aiohttp-3.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045", size = 1929915, upload-time = "2026-10-11T00:59:55.609Z" },
{ url = "https://files.pythonhosted.org/packages/33/ad/f80e8d33933d0eacd0217efa3b7fb32c9f48ed480ed0db53cf9948bf474f/aiohttp-3.14.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1", size = 1773832, upload-time = "2026-10-11T00:59:57.611Z" },
{ url = "https://files.pythonhosted.org/packages/04/a4/0273d239f3e3bb69438f209c64c82e1f98dc60d5db264795e41f7dd05dcb/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e", size = 1901001, upload-time = "2026-10-11T01:00:00.605Z" },
{ url = "https://files.pythonhosted.org/packages/16/ed/7dd439d26c654645bc34ca3c9a822fc11c1c51801fed1cefdfe1cc41c1f6/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e", size = 1907218, upload-time = "2026-10-11T01:00:02.699Z" },
{ url = "https://files.pythonhosted.org/packages/32/dd/86249c3b8248562a17fd3e22ee0c164378d65766f01aceef62cf52783710/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793", size = 1958650, upload-time = "2026-10-11T01:00:04.851Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0b/ca4d53d68f683ce195807fd0aeb063bf6d1c6b39343fff234c9524e8d5a9/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20", size = 1760057, upload-time = "2026-10-11T01:00:06.978Z" },
{ url = "https://files.pythonhosted.org/packages/6b/42/005437ffd7fa56c2ce3347add40404b654a32754ce91f2e2a14b9e5ab2bc/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703", size = 1978081, upload-time = "2026-10-11T01:00:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/03/71/3a5b66fe1b7b23d3c6524350817ed55de5feb7fbb4cccba7cbe044d118dd/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e", size = 1922954, upload-time = "2026-10-11T01:00:11.035Z" },
{ url = "https://files.pythonhosted.org/packages/69/84/d08a554f281d42fd7c6b2b6dd7738f3e3a7a9f657ddd87e11ca2091cdfe3/aiohttp-3.14.5-cp311-cp311-win32.whl", hash = "sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329", size = 488101, upload-time = "2026-10-11T01:00:12.933Z" },
{ url = "https://files.pythonhosted.org/packages/0f/15/52b5e65f02b33686ae49f1518548ae4f5a7adaf31d9d3c54fa12a143137b/aiohttp-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc", size = 512575, upload-time = "2026-10-11T01:00:14.743Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b9/bb75012635f3defb0f7cbb7c4ae391e36bb9cb3fbdca3b9944e2ebebf743/aiohttp-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755", size = 493179, upload-time = "2026-10-11T01:00:16.961Z" },
{ url = "https://files.pythonhosted.org/packages/d5/94/6ba86efddcb616c811b40e6a0dfdd862738f647e4e3860a961075d5e9ed8/aiohttp-3.14.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450", size = 817499, upload-time = "2026-10-11T01:00:18.901Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e1/7bca6d84dabd228aa8eb4b7f9feac2586aaa9be5505d7d65bf287a615c43/aiohttp-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8", size = 549467, upload-time = "2026-10-11T01:00:20.854Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/11b89f45ca486252dd67dd5f3231fec04bf5518da39a95cb3997619f17fb/aiohttp-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2", size = 548067, upload-time = "2026-10-11T01:00:22.659Z" },
{ url = "https://files.pythonhosted.org/packages/22/ff/c6615806c14aab34f82b9424ccde8ce6e417315fd57ce1c5b4d4747888e1/aiohttp-3.14.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764", size = 1896245, upload-time = "2026-10-11T01:00:24.638Z" },
{ url = "https://files.pythonhosted.org/packages/39/b2/25a8c971ae6a92c8394d77e42422d7f38989e05cf41a5ceb92d73d67ab7e/aiohttp-3.14.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820", size = 1872180, upload-time = "2026-10-11T01:00:26.838Z" },
{ url = "https://files.pythonhosted.org/packages/2f/d5/99f93ea36cc5205e47c1e5a803e087f2ad21b5430b5db2e942cb6e988a37/aiohttp-3.14.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835", size = 1937261, upload-time = "2026-10-11T01:00:28.74Z" },
{ url = "https://files.pythonhosted.org/packages/40/a6/9ac9c9e6695040bd73d2584a1b59a9388f6433c76d5294a7bf591e21ffe5/aiohttp-3.14.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02", size = 2064994, upload-time = "2026-10-11T01:00:30.623Z" },
{ url = "https://files.pythonhosted.org/packages/da/e4/aa172eb534b7f02f1f8ff1c3213347eaf3cf218db91a727c6863c22f1035/aiohttp-3.14.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584", size = 1919630, upload-time = "2026-10-11T01:00:32.548Z" },
{ url = "https://files.pythonhosted.org/packages/06/7d/4eedafc5bababa8932636c141e346806966eade12c0b7e5946d43bf8218b/aiohttp-3.14.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d", size = 1710016, upload-time = "2026-10-11T01:00:34.471Z" },
{ url = "https://files.pythonhosted.org/packages/b2/94/eee018537ba19da0ceb2ac79cab83faed4ac49568e08376e2799043f2538/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30", size = 1858112, upload-time = "2026-10-11T01:00:36.581Z" },
{ url = "https://files.pythonhosted.org/packages/68/76/354653a306547238f3427283905972d796ba7c292ba9977abec9f2b6f260/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073", size = 1896371, upload-time = "2026-10-11T01:00:38.478Z" },
{ url = "https://files.pythonhosted.org/packages/f2/ec/63e8c7136b570e356345ad3174e3820fdc973ea10712cb6c649bf875755a/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794", size = 1905836, upload-time = "2026-10-11T01:00:40.527Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/11365bda144b127928cd42533d0eff78a55613c9e28f81941bd6630ea887/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd", size = 1696534, upload-time = "2026-10-11T01:00:42.686Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3d/82df0461b18e00b2998f205c03e0d3010222478c43640aceb8e03dcd7e8f/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d", size = 1933970, upload-time = "2026-10-11T01:00:44.71Z" },
{ url = "https://files.pythonhosted.org/packages/03/ad/6ddfe0aacd931c17b53533336d97e9d11a98b96d6ae815a9da0b19f82ccf/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748", size = 1899501, upload-time = "2026-10-11T01:00:46.629Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8e/189bdd9ae4793059bb09f6dc880f211a6c6c7859cebcbf33912dbfab7dd7/aiohttp-3.14.5-cp312-cp312-win32.whl", hash = "sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1", size = 480370, upload-time = "2026-10-11T01:00:48.468Z" },
{ url = "https://files.pythonhosted.org/packages/ae/ce/1f08114679d49655b30a6e0a29858375c94b82c1de1a0bd0a20c2fee8b02/aiohttp-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111", size = 506141, upload-time = "2026-10-11T01:00:50.272Z" },
{ url = "https://files.pythonhosted.org/packages/79/d4/c7b4f60b16a1b7e43249fa9031ae05e7e8c341ba4b7d1866f914dafeaa0e/aiohttp-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac", size = 488108, upload-time = "2026-10-11T01:00:52.321Z" },
{ url = "https://files.pythonhosted.org/packages/68/30/173960c42b05a6c59f7558e4b12a4b0d9ba376cf6aa9bde7f9e08a30ca8d/aiohttp-3.14.5-py3-none-any.whl", hash = "sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b", size = 279517, upload-time = "2026-10-11T01:05:08.523Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
{ url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
{ url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
{ url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
{ url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
{ url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
{ url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
{ url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
{ url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
{ url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
{ url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
{ url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
{ url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
{ url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
{ url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
{ url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
{ url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
{ url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
{ url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
{ url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
{ url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
{ url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
{ url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
{ url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
{ url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
{ url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
{ url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
]
[[package]]
@@ -1849,11 +1848,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.10.19"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
@@ -2345,15 +2344,15 @@ wheels = [
[[package]]
name = "django-allauth"
version = "65.19.7"
version = "65.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/33/7c8f8fc85f4ee644ee3acb20c1fe4d04516298ab5903f812e1556b65f947/django_allauth-65.19.7.tar.gz", hash = "sha256:c7749551b659ca954e483f6f634cd0c262d65dd8144f5219b3a31cba0426e981", size = 2296178, upload-time = "2026-10-01T12:35:24.152Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/c1/d3385f4c3169c1d6eea3c63aed0f36af51478c1d72e46db12bb1a08f8034/django_allauth-65.15.0.tar.gz", hash = "sha256:b404d48cf0c3ee14dacc834c541f30adedba2ff1c433980ecc494d6cb0b395a8", size = 2215709, upload-time = "2026-03-09T13:51:28.675Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/c4/a42cc4439b18b8d2767f8c54999e011b9f620a478806ed265baed7c3dc01/django_allauth-65.19.7-py3-none-any.whl", hash = "sha256:8899377f38afabf10445c9ee2f808a730304ca8aecc540657d51a8b3928283d6", size = 2076429, upload-time = "2026-10-01T12:35:20.844Z" },
{ url = "https://files.pythonhosted.org/packages/75/b8/c8411339171bd8bc075c09ef190fb42195e9a2149e5c5026e094fe62fce0/django_allauth-65.15.0-py3-none-any.whl", hash = "sha256:ad9fc49c49a9368eaa5bb95456b76e2a4f377b3c6862ee8443507816578c098d", size = 2022994, upload-time = "2026-03-09T13:51:19.711Z" },
]
[package.optional-dependencies]
@@ -2510,14 +2509,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.18.3"
version = "3.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/b2/1cee76b188301f7d03169ca77fab2b3d3ec5d01b3469189f944af2d7f11a/djangorestframework-3.18.3.tar.gz", hash = "sha256:446a9b352e7eff630421ab3f2328bd2401b109a9470afa4a31189994911ed030", size = 923563, upload-time = "2026-10-06T21:33:51.521Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420, upload-time = "2024-06-19T07:59:32.891Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/dd/de5f4543765bec94e036a5e9593a42f8fa7efec8de78eb56b9ca834385f3/djangorestframework-3.18.3-py3-none-any.whl", hash = "sha256:8544bb674846731b1e3c9b309236ee1dc412905a0aa725be2ec193ca950a7d12", size = 903986, upload-time = "2026-10-06T21:33:49.917Z" },
{ url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235, upload-time = "2024-06-19T07:59:26.106Z" },
]
[[package]]
@@ -2550,11 +2549,11 @@ wheels = [
[[package]]
name = "dnspython"
version = "2.9.0"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
@@ -4204,7 +4203,7 @@ wheels = [
[package.optional-dependencies]
broker = [
{ name = "pymsalruntime", marker = "sys_platform == 'win32'" },
{ name = "pymsalruntime" },
]
[[package]]
@@ -4419,11 +4418,11 @@ wheels = [
[[package]]
name = "oauthlib"
version = "4.0.0"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/a1bcc8ba112a627f8ffbdc212a78ce18d3ac07e91a5ca65d27918eee25a1/oauthlib-4.0.0.tar.gz", hash = "sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86", size = 187232, upload-time = "2026-09-28T06:01:18.77Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/f4/78229a1066068ca14fc60fb26cf7381cabe4382261392b90e5f9552722d4/oauthlib-4.0.0-py3-none-any.whl", hash = "sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd", size = 159715, upload-time = "2026-09-28T06:01:17.008Z" },
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
]
[[package]]
@@ -4637,38 +4636,33 @@ wheels = [
[[package]]
name = "pillow"
version = "13.0.0"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/d7/af6af1b8cc55e07cb0489d5b0950bf5a9bb1461eeba825700b6d8d0e26ea/pillow-13.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:6f38f3f885a09cafc93fd667b1173a92f379e43785d635e7983c893f0da319fb", size = 5592324, upload-time = "2026-10-16T22:15:24.672Z" },
{ url = "https://files.pythonhosted.org/packages/0b/d2/2c12cb4fbb5d95f38d1001ff3d95fafcb830077018cba40e56a8f932dfbc/pillow-13.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37290fbef0c73e2d51d594eb78fded7dc7170d1c3ad0a890269e6fd8c8c4e061", size = 4856298, upload-time = "2026-10-16T22:15:27.669Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ee/229b27472483a4801ea94990815e710391836afdf987b4b722b62bd75db6/pillow-13.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c88c10dd432f3b39ccdafc9ed81d0c21b8fd634502574da76d1bfa4fe2c76d0", size = 6517304, upload-time = "2026-10-16T22:15:29.43Z" },
{ url = "https://files.pythonhosted.org/packages/36/46/f509b74787f40ce0627501089d630b2c741b63b244c1f40b640b3b01e181/pillow-13.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a625df9a2a5597fa5e5266e01aefc82604bb85d291e3547981e863560d75da6c", size = 7140667, upload-time = "2026-10-16T22:15:31.368Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/5a398b8ec2c37145397d6461c0b551c23ec03072aa64f357aed25e582b00/pillow-13.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:10b5019883c98f1dd819ca48ffc6f33a91cee04948fe637c605f4345c7cafd6f", size = 6610583, upload-time = "2026-10-16T22:15:34.025Z" },
{ url = "https://files.pythonhosted.org/packages/4f/7b/f2f1109b230fd4e2d57ab2305de31adc37ea9b4ff8719436c623cb484415/pillow-13.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f391dc92a015c478745192fe4f2e120c2af2aba18d9fe0616341cbf537599806", size = 7218241, upload-time = "2026-10-16T22:15:36.214Z" },
{ url = "https://files.pythonhosted.org/packages/ab/d3/069edb9083b267746baf59756282cacef904d3475c5288c7216fb5195a98/pillow-13.0.0-cp311-cp311-win32.whl", hash = "sha256:6a0920cb55df5a4d3f6862964908078c0bd59332bdd1bcd20d14e8c70ef27cc5", size = 6515278, upload-time = "2026-10-16T22:15:38.129Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f6/6e2d96413e4c9dbec12ea584d9e646d602877d4cce3ea669099f959deac6/pillow-13.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:d7710eb676e9bb68965cb045be0d333b4c0d7f9dfbaa7fbf121385b1588683d3", size = 7278471, upload-time = "2026-10-16T22:15:40.286Z" },
{ url = "https://files.pythonhosted.org/packages/70/e7/baff21f8cb60496c8c250609501b88919eeedb4088d3da106f6c62f3f3c0/pillow-13.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:12fbb851cf323cc19aac3eea1df5ad17b57e91e9fe3cfd06100dba7bb8208c13", size = 2698981, upload-time = "2026-10-16T22:15:42.637Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b8/58d79332229482eb0d27c82973850808c19d37e5bdc14443d3417bd373f5/pillow-13.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79", size = 5568643, upload-time = "2026-10-16T22:15:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/39/4b/f2ed53d83aa44ac19599ac0a54897f867fafb95f406980d1e24083f8cbe3/pillow-13.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52", size = 4852329, upload-time = "2026-10-16T22:15:46.284Z" },
{ url = "https://files.pythonhosted.org/packages/3b/bb/9e30e8ed18c20800ee327a12107d5249ad8dc444867b3adfcc21654e3c71/pillow-13.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d", size = 6519309, upload-time = "2026-10-16T22:15:48.14Z" },
{ url = "https://files.pythonhosted.org/packages/e6/99/fdf65cc539e266ab93546c261bdf98411fe165c6e4e13f9dbf597097192b/pillow-13.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca", size = 7144542, upload-time = "2026-10-16T22:15:50.093Z" },
{ url = "https://files.pythonhosted.org/packages/26/9b/f38b7c4d8f599537e2d1a6d546cec9beef7536ff8f5c49a3f9c78179d8ac/pillow-13.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620", size = 6618042, upload-time = "2026-10-16T22:15:52.054Z" },
{ url = "https://files.pythonhosted.org/packages/a8/7c/45ee28531f89ee4582967d8a17f7a26d71ca64cd902b93c269590cd60a00/pillow-13.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40", size = 7228137, upload-time = "2026-10-16T22:15:54.009Z" },
{ url = "https://files.pythonhosted.org/packages/01/bc/9c2a969cb1b70201d04fa17fd5aedf02a732870b8da86bceaaa30b0ca385/pillow-13.0.0-cp312-cp312-win32.whl", hash = "sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27", size = 6515423, upload-time = "2026-10-16T22:15:56.25Z" },
{ url = "https://files.pythonhosted.org/packages/ff/cd/73a79ecdc822076002eb24b5e1e4a530b0bb5310fcb6d239abbec877110b/pillow-13.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b", size = 7272166, upload-time = "2026-10-16T22:15:58.597Z" },
{ url = "https://files.pythonhosted.org/packages/4a/a3/be5a1236efd085b3984e4305da8f179ae9dab80c991d2ad78f2bd5c4c843/pillow-13.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51", size = 2698943, upload-time = "2026-10-16T22:16:00.637Z" },
{ url = "https://files.pythonhosted.org/packages/68/c0/fb7b1caf25ed5ac5d46116f09b8b0706b9559833c65c667cecb1a53153f2/pillow-13.0.0-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:405be6058e206a86fedce2f715ecc2343f94072d7925251632dffa2c05f70dca", size = 5521630, upload-time = "2026-10-16T22:17:50.008Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e6/4d11d5b1137f4fc080605cce410cfaae4f303ad400bdec75d27d3083fa29/pillow-13.0.0-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:b84be41387fc00db28c13c02e30d42b02d8d8f6a4a741a1aee6012ca7f1f6039", size = 4806023, upload-time = "2026-10-16T22:17:51.932Z" },
{ url = "https://files.pythonhosted.org/packages/f1/45/0923202a658e8ff6aafd72c1a4395ebadcdebaa182c437d8595d3dd3db32/pillow-13.0.0-pp311-pypy311_pp80-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a192360d512305fe92d3053e4f7627bbad1f7708f1f4ad38cccd9336acabe9ad", size = 5464831, upload-time = "2026-10-16T22:17:53.742Z" },
{ url = "https://files.pythonhosted.org/packages/07/52/d3e38f0cad1dadea2a658b9a7d6ba8a04048f5ae14f011b806e5c3bd6bcb/pillow-13.0.0-pp311-pypy311_pp80-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83b37abd87b590acd8f47ca8a61f70893322b11834d36e07c2298dacad4d14d2", size = 6043133, upload-time = "2026-10-16T22:17:55.625Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f9/2ed7685980c1d64f8131a7d87a19c369424747577152622b73ddeff163b7/pillow-13.0.0-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:73ee25cf8cf8b75d5a5aeed7cb6588f5e5dbfbb0fde51707b630bbb23897645c", size = 7365208, upload-time = "2026-10-16T22:17:57.509Z" },
{ url = "https://files.pythonhosted.org/packages/60/da/f6d644e631c5b370c5d0b899f3ccadd2780aab070b29a110527cf4e1fd2c/pillow-13.0.0-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463", size = 5523188, upload-time = "2026-10-16T22:17:59.334Z" },
{ url = "https://files.pythonhosted.org/packages/e4/b0/2594f202d743a35d4fe80682bfd96f277e5ca7aecb27cf64ef5bfc1d000b/pillow-13.0.0-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e", size = 4806808, upload-time = "2026-10-16T22:18:01.188Z" },
{ url = "https://files.pythonhosted.org/packages/8e/ed/d504165ef79af6fafe25c76726cf30f544cc236002f7ed054f1d51adc4ff/pillow-13.0.0-pp312-pypy312_pp80-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63", size = 5464358, upload-time = "2026-10-16T22:18:02.779Z" },
{ url = "https://files.pythonhosted.org/packages/7b/fe/a10aba5503ca13e9e009b8bd0a1a4ce5584bdce246245fd6f3a83701369c/pillow-13.0.0-pp312-pypy312_pp80-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61", size = 6043514, upload-time = "2026-10-16T22:18:04.538Z" },
{ url = "https://files.pythonhosted.org/packages/c0/37/66b12dc476041fe523bca29c9d58fbf1ea92a4eaf73acfb9e993948f1191/pillow-13.0.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", size = 7366030, upload-time = "2026-10-16T22:18:06.638Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
@@ -4945,7 +4939,7 @@ dependencies = [
[[package]]
name = "prowler-api"
version = "1.49.0"
version = "1.46.0"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -5020,7 +5014,7 @@ requires-dist = [
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "dj-rest-auth", extras = ["with-social", "jwt"], specifier = "==7.0.1" },
{ name = "django", specifier = "==5.2.17" },
{ name = "django-allauth", extras = ["saml"], specifier = "==65.19.7" },
{ 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" },
{ name = "django-cors-headers", specifier = "==4.4.0" },
@@ -5029,7 +5023,7 @@ requires-dist = [
{ name = "django-filter", specifier = "==24.3" },
{ name = "django-guid", specifier = "==3.5.0" },
{ name = "django-postgres-extra", specifier = "==2.0.9" },
{ name = "djangorestframework", specifier = "==3.18.3" },
{ name = "djangorestframework", specifier = "==3.15.2" },
{ name = "djangorestframework-jsonapi", specifier = "==7.0.2" },
{ name = "djangorestframework-simplejwt", specifier = "==5.5.1" },
{ name = "drf-nested-routers", specifier = "==0.95.0" },
@@ -5371,33 +5365,33 @@ wheels = [
[[package]]
name = "pymongo"
version = "4.18.3"
version = "4.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/d8/2421a5ae0d6dcdaad2a0fb75d4071eaede9f764e73b829c62b6185c3ee6b/pymongo-4.18.3.tar.gz", hash = "sha256:5dd6e659b6014288a1c53458929402a58f44a032e6f29bcef44e7477c5268e48", size = 2747872, upload-time = "2026-10-08T19:44:08.343Z" }
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/58/a6/63bdeb527d98998b8ea2c667eca00d48f22dd42af281e9a2d7090632d54d/pymongo-4.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f00cb357d7cc7f2798116e2377732a409c43a6dc882f0241eafed7ffed50655", size = 818805, upload-time = "2026-10-08T19:42:09.125Z" },
{ url = "https://files.pythonhosted.org/packages/d0/e9/35602972d9fa98b894d1e5feef4db2f5d275298430e99b54b39f01125efa/pymongo-4.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fe2ef9c6eb6b75689e10b20a3d8119da87302481b0a7029f9399b35142adfd8", size = 819303, upload-time = "2026-10-08T19:42:10.749Z" },
{ url = "https://files.pythonhosted.org/packages/d8/e1/468f2c69b32565c93a56623535f248b091cf52e83c3509fa4b791427889f/pymongo-4.18.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6090d4bed582c97e38fa818c0a2b7443f203cb28882900b433ff713465f158", size = 1023754, upload-time = "2026-10-08T19:42:12.683Z" },
{ url = "https://files.pythonhosted.org/packages/e1/24/8af75e8af2427a47cfcc996444df934990ac4890d855f2ec064409d8b94b/pymongo-4.18.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f9903d0a089317422f52bbc25f5827e6656f0c42c43ed7d799bd02748e79a1", size = 1031920, upload-time = "2026-10-08T19:42:14.443Z" },
{ url = "https://files.pythonhosted.org/packages/78/d0/96fa79fb7cb47e6f09e58ccbe1a726330b5d052f395b65d57e286849ed43/pymongo-4.18.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac9bf2304c2b092ccf04261ab0cddb7fd65df1cc1ae0fa57312b03396c00d28c", size = 1053859, upload-time = "2026-10-08T19:42:16.315Z" },
{ url = "https://files.pythonhosted.org/packages/4d/99/1b3f48bd3580c53e4a0e89bdc8cd8c15af94ca944f9de96582cb3ebfa5d0/pymongo-4.18.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f37095428af3042f6bb1ebe269fedcbb645d9e0642b274e1cff026d3979500b", size = 1047418, upload-time = "2026-10-08T19:42:18.023Z" },
{ url = "https://files.pythonhosted.org/packages/08/1e/ab9148b15dcefd3d02852a65e4ac3a1df86248c227dd532dc53b41f33697/pymongo-4.18.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16ade5053ab6c712fd25d3f878e38441b169d607d1326d708844a131911d029f", size = 1033358, upload-time = "2026-10-08T19:42:19.746Z" },
{ url = "https://files.pythonhosted.org/packages/39/93/bbbd0edfa33b10e7a86c864478dbb12163c946c142fd1e7b1bd0b6490353/pymongo-4.18.3-cp311-cp311-win32.whl", hash = "sha256:463c09e2cc208a65d35a1af3c613360cff6d58c8aef652273da07250bb214dba", size = 814425, upload-time = "2026-10-08T19:42:21.501Z" },
{ url = "https://files.pythonhosted.org/packages/1c/13/7515f91ed9e80968cc5dc9321ac97fafde07afb75123adb9e294f50c8a10/pymongo-4.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:1d7d0474012def6113c224b167aae661b926ac3b788219426830013ea25acd33", size = 821387, upload-time = "2026-10-08T19:42:23.349Z" },
{ url = "https://files.pythonhosted.org/packages/89/59/f54d5ee7d95ec4ed1f31bff014a0f61caa3e888d7a89a0585f3eb4be164b/pymongo-4.18.3-cp311-cp311-win_arm64.whl", hash = "sha256:83dff65baa6f2423857598ffc371d7412fa4d2a07c618bdc8d5053ade65de664", size = 817697, upload-time = "2026-10-08T19:42:25.128Z" },
{ url = "https://files.pythonhosted.org/packages/05/d5/4775a2891396ad125545e23b3024adae4bfac9553b70c924f1f372269dbf/pymongo-4.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea78719dd05de3a919a52b94bec790c0d0cb7d07d2f7271711832664502a0782", size = 819374, upload-time = "2026-10-08T19:42:26.931Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0b/89ad56f43c3da6cbde100699f6b99528e78eba3c6740d8dad4ea2516aa45/pymongo-4.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6029d14761ba7243e6c5e464592013b519ad4dd3e4cfb75ddec39f4b5910711b", size = 819694, upload-time = "2026-10-08T19:42:28.76Z" },
{ url = "https://files.pythonhosted.org/packages/84/b4/b68ffc205441b0a6d36d6299e35e063a5d0d3264fd685428920e1f82b634/pymongo-4.18.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9536fb3820f721290f03ad07472ec2266d8f364f91de628679a7146c9c1dbe35", size = 1040136, upload-time = "2026-10-08T19:42:30.852Z" },
{ url = "https://files.pythonhosted.org/packages/c1/40/e779ff3d9165316c35a2f9742a42b9c3e3a678e9e2a9f6fe4128b7c551eb/pymongo-4.18.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e461bfca4861057929efa4215730b28b93b2adb4d07828d0b65475755bbf63f5", size = 1051430, upload-time = "2026-10-08T19:42:32.533Z" },
{ url = "https://files.pythonhosted.org/packages/07/9b/443ee038a739cc65a75f2078c9ef725c1cb4881545d2e9d7941c46f64a6c/pymongo-4.18.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1fef248623ed5e7406902a68d49dc0b1db434f19489f8d2fc9fe512c3c08bb1", size = 1075464, upload-time = "2026-10-08T19:42:34.325Z" },
{ url = "https://files.pythonhosted.org/packages/36/4b/d80518f675cd4c1215b770444bb83002454574dae0e69760af10703ed1e8/pymongo-4.18.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:213eaed8fc4f2b0f9c84323a229dea699e01e18b8fb39723f430123b6ee77813", size = 1069046, upload-time = "2026-10-08T19:42:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/e5/77/f2e9648c62e423c3b9dab1e16491a6c33250487c819e5c75784b35d16047/pymongo-4.18.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa6f363ff648bf061335d2190dd580cbf465b1308a7e6acb992d128d6a16a3bd", size = 1050321, upload-time = "2026-10-08T19:42:38.052Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e4/3236e3a87b29fc4c502ad7dd1521c20d4a29faf1db6fde6ae94d05c5ec27/pymongo-4.18.3-cp312-cp312-win32.whl", hash = "sha256:28ba8cae86ea02d7ffdf0eea81be69be80d35d6a4a3eba4dc436d3194341805a", size = 815300, upload-time = "2026-10-08T19:42:40.062Z" },
{ url = "https://files.pythonhosted.org/packages/1e/18/3fa9d86ba32386c02ea991f10875a6a066dd5e5d80790243be3c141b0e73/pymongo-4.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc8ccf72b76c99a6b9fd05f8b89fe4a693128c5cfdba70f70e5792a6a563f6b0", size = 822079, upload-time = "2026-10-08T19:42:42.089Z" },
{ url = "https://files.pythonhosted.org/packages/03/50/65a7cefd3891b77994841992b2c5b59394667df64ef21377b8ac7ecdef47/pymongo-4.18.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a1f7c7dc1d554449a1695d897eb42b6080a2f1e9ccd81385dfa00204979c54d", size = 817935, upload-time = "2026-10-08T19:42:43.98Z" },
{ 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]]
@@ -6573,50 +6567,48 @@ wheels = [
[[package]]
name = "yarl"
version = "1.25.1"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" },
{ url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" },
{ url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" },
{ url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" },
{ url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" },
{ url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" },
{ url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" },
{ url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" },
{ url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" },
{ url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" },
{ url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" },
{ url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" },
{ url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" },
{ url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" },
{ url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" },
{ url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" },
{ url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" },
{ url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" },
{ url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" },
{ url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" },
{ url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" },
{ url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" },
{ url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" },
{ url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" },
{ url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" },
{ url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" },
{ url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" },
{ url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" },
{ url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
{ url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
{ url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
{ url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
{ url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
{ url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
{ url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
{ url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
{ url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
{ url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
{ url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
{ url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
{ url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
{ url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]
[[package]]
@@ -1,38 +1,22 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "prowler",
"displayName": "Prowler Cloud",
"version": "0.1.0",
"description": "Connect Claude to Prowler Cloud through the Prowler MCP server. Reads providers, scans, findings, compliance results and resources for AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and the other providers listed at https://registry.prowler.com, and returns remediation guidance for failed checks. Writes are limited to Prowler Cloud itself: triggering scans, finding triage, mute rules and the mutelist, providers and integrations. Your cloud resources are not changed by the connector.",
"description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.",
"author": {
"name": "Prowler",
"email": "support@prowler.com",
"url": "https://prowler.com"
},
"homepage": "https://docs.prowler.com/user-guide/ai-agents",
"homepage": "https://docs.prowler.com",
"repository": "https://github.com/prowler-cloud/prowler",
"license": "Apache-2.0",
"keywords": [
"alibabacloud",
"aws",
"azure",
"cloud-security",
"cloudflare",
"compliance",
"gcp",
"github",
"huaweicloud",
"iac",
"kubernetes",
"m365",
"mcp",
"mongodbatlas",
"okta",
"openstack",
"oraclecloud",
"prowler",
"security",
"vercel"
"compliance",
"cloud-security",
"mcp"
],
"userConfig": {
"api_key": {
+5 -7
View File
@@ -1,11 +1,9 @@
{
"mcpServers": {
"prowler": {
"type": "http",
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer ${user_config.api_key}"
}
"prowler": {
"type": "http",
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer ${user_config.api_key}"
}
}
}
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright @ 2024 Toni de la Fuente
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3 -17
View File
@@ -1,17 +1,9 @@
# Prowler Cloud
# Prowler for Claude Code
Cloud security and compliance in Claude, powered by the [Prowler MCP server](https://docs.prowler.com/projects/prowler-mcp/). Claude reads your Prowler Cloud providers, scans, findings and compliance results, returns Prowler's remediation guidance for each failed check, and manages triage and mute state. The connector never changes your cloud resources: any fix is applied with your own tools (Terraform, a cloud CLI or the console), and only after you approve it.
End-to-end cloud security and compliance from inside [Claude Code](https://www.claude.com/product/claude-code), powered by the [Prowler MCP server](https://docs.prowler.com/projects/prowler-mcp/). The plugin lets Claude walk a Prowler Cloud-connected account through a compliance assessment and remediate findings until the chosen security or industry framework is compliant.
> **Preview**: this plugin is under active development. Report issues at <https://github.com/prowler-cloud/prowler/issues> or join the [Slack community](https://goto.prowler.com/slack).
## What it does
Prowler Cloud connects Claude to your cloud security posture. The bundled `framework-compliance-triage` skill checks that a provider account is connected to Prowler Cloud, reads the latest scan and compliance report for the framework you pick (CIS, PCI-DSS, NIST, Prowler ThreatScore and more), and then walks you through fixing each failed requirement. Every change is proposed first and applied only after you approve it.
## Supported providers
Any provider Prowler Cloud supports, including AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub, Google Workspace, Cloudflare, Oracle Cloud, Alibaba Cloud, Huawei Cloud, OpenStack, Linode, Scaleway, STACKIT, E2E Networks, NHN Cloud, MongoDB Atlas, Okta, Vercel, VCF, Infrastructure as Code, container images and LLMs. The full, growing list lives in the [Prowler Registry](https://registry.prowler.com/providers). Prowler also supports custom providers, so you can add your own and use them with this plugin.
## Requirements
- [Claude Code](https://www.claude.com/product/claude-code) installed and signed in.
@@ -20,8 +12,6 @@ Any provider Prowler Cloud supports, including AWS, Azure, GCP, Kubernetes, Micr
## Installation
From the Claude directory, open **Customize > Plugins > Discover**, search for **Prowler Cloud** and select **Add**. In Claude Code you can also install it from the command line:
Inside a Claude Code session:
```text
@@ -68,10 +58,6 @@ You pick a **primary tool** (Terraform, gh / az / aws CLI, web console, or mixed
Claude tracks progress in a markdown report under `.prowler/` at your project root — one file per framework × account. Open it any time to see exactly where the flow is. When all findings are addressed, Claude proposes a fresh Prowler scan to verify everything end-to-end.
## Data and privacy
The plugin stores nothing itself. It connects to the Prowler MCP server at `https://mcp.prowler.com/mcp`, authenticating with the API key you provide (kept in your system keychain and sent only as a bearer token to that server). Through it, Claude reads your Prowler Cloud providers, scans, findings, compliance results and resource metadata. It can also write to Prowler Cloud: trigger scans, set finding triage, create mute rules, replace the mutelist, and add or remove providers and integrations. Mutes and deletions are marked as destructive, so your client can ask before running them. It also queries the public Prowler Hub and Prowler documentation. The remediation report is written locally under `.prowler/` in your project. Nothing is sent to any other service. See the [Prowler privacy policy](https://prowler.com/privacy-policy) for how Prowler Cloud handles account data.
## Uninstalling
```text
@@ -91,4 +77,4 @@ The stored API key is removed automatically.
## License
Apache 2.0 — see [LICENSE](LICENSE).
Apache 2.0 — see [LICENSE](../../LICENSE).
+1 -24
View File
@@ -164,28 +164,6 @@ mainConfig:
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Elastic Beanstalk Configuration
# aws.elasticbeanstalk_environment_php_platform_updated
# Oldest PHP branch still receiving security fixes
elasticbeanstalk_php_min_branch: "8.2"
# Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release
elasticbeanstalk_php_min_platform_version: "4.13.13"
# AWS Lightsail Configuration
# aws.lightsail_instance_php_blueprint_updated
# Blueprint id prefixes that ship PHP and require manual runtime verification
lightsail_php_blueprints:
[
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
@@ -523,9 +501,8 @@ mainConfig:
shodan_api_key: null
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.5"
php_latest_version: "8.2"
# azure.app_ensure_python_version_is_latest
python_latest_version: "3.12"
# azure.app_ensure_java_version_is_latest
+1 -1
View File
@@ -86,7 +86,7 @@ Before installing the Helm chart, you must create a Kubernetes Secret containing
NEO4J_AUTH: "neo4j/[prowler-password]"
```
> **Note:** You can use the [example secrets file](./examples/minimal-installation/secrets.yaml) as a template, but **always replace the placeholder values with your own secure keys** before applying.
> **Note:** You can use the [example secrets file](./examples/minimal-installation/secrets.yaml) as a template, but **always replace the placeholder values with your own secure keys** before applying. Earlier revisions of that example shipped working values; the API refuses to start with any key that was published in this repository, so a deployment still using one must generate a new one.
3. **Apply the secret to your cluster:**
@@ -4,54 +4,28 @@ type: Opaque
metadata:
name: prowler-secret
stringData:
# Every value below must be generated for your own installation. The values that
# used to ship here were public, and the API refuses to start with them.
# openssl genrsa -out private.pem 2048
DJANGO_TOKEN_SIGNING_KEY: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCIro0QiLAxw7rF
GO0NgAWJfkpYE5ysMGDCbId07HUrv+/SCoRjqKVzGJVIvmNP5oByzSehPgswW9v3
3dqe2r9sCS1JyMa+XO3qfZCR0uRDcPCwZjIyr0QQLpWAymdBa8baeHsU1/3Orjcb
Vrr+lNx4HQJOiSn094iXPReW/25hYeq/SXs79V2CR87PGdoZAhb8IllAxJgdfkeB
/iWohY/1vfRTmIuMweWGXk0aKzPsBdvE/DqG4HjiNVEPh18G3vid0YTZNmm7u8vO
Cue3x9NQWGHA4QtxNtLtxlHcOEryqZ9ChO2nC+ew0Xl/v706XFNyLFicjisIKNQo
qdkaMS33AgMBAAECggEAGdJIChCYoL4mYafk2MEPyrrWFq+V0J3PGcvhB0DInfxD
tT2RZzZsE0NYqIZ3Qpf8OjPxwa9z863W74u1Cn+u3B0bti29BieONteD4VijEO6c
OecEorijth7m1Y7nVN+kkI9kSTrI0yvsczi+WOwMfpCUZ/vXtlSxNEkxVLBqzPCo
9VxAFIjgWOj2rpw8nxPedves36PUrC5ghLqrOTe1jmw/Di0++47AXG+DsTXc00sc
5+oybopm3Kimsxrqbf9s8SZf2A8NiwqcbLj8OtP2j2g4TCEgZYLD5Zmt+JN/wN4B
WsQG/Hwp4KPPm9QTHEpuuoPFP1CZWZeq8gPcV4apYQKBgQC+TuXjJCYhZqNIttTZ
z/i3hkKUEKQLkzTZnXaDzL5wHyEMVqM2E/WkilO0C9ZZwh0ENPzkp+JsHf7LEhHy
wSHOti81VzUCjN/YpCBKlOlClqSiDlOonImrobLei8xgvmA0VmGtirCXZyyzZUoV
OyPr17WpK6G/M5piX59MvKQg0QKBgQC33NBoQFD8A6FjrTopYmWfK099k9uQh9NE
bvUYsNAPunSDslmc/0PPHQC7fRX5Ime2BinXAN1PYtB/Fsu3jv/+FCUM5hVil0Dd
KBvt13+RYSCJKlhcGP1EkWoIg1F2XXBOZKJrC8VQ+Vyl2t06UcWQqy5M9J4VZaqI
fruOLU/URwKBgE55GjJfZZnASPRi78IhD94dbra/ZeWf/dr+IzCV7LEvJOGBmCtk
b5Y5s+o6N1krwetKLj3bPHJ4q+fwu5XuLZKfbTgBjcpPbL5YbzhRzx22IIzye2y7
n8k2FBvQaaY62lC6jeyRk9/am4Qd8D5w9I77k9z+MOQ20yJda8KoxsUBAoGBAIQ9
5QPmppjsf4ry0C9t30uhWhYnX7fPiYviBpVQrwVxBVan076Q9xOjd6BicohzT4bj
XfqPW546o12VZsbKqqLzmEZzwpPb2EJ5E8V4xv8ojb86Xr03GArWUB55XQE2aY1o
4kz99VitUg7UoWPN5ryL8sxU8NLRAdwU0w+K1a0HAoGAZaU7O94u9IIPZ6Ohobs2
Vjf/eV0brCKgX61b4z/YhuJdZsyTujhBZUihZwqR696kiFKuzmHx1ghE2ITvnPVN
q0iHxRZzBCnRQ+mQlS0trzphaCP0NVy3osFeAD9mJfnOnSmkU0ua4F81mkvke1eN
6nnaoAdy2lmMr96/Tye2ty4=
REPLACE WITH THE CONTENTS OF private.pem
-----END PRIVATE KEY-----
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiK6NEIiwMcO6xRjtDYAF
iX5KWBOcrDBgwmyHdOx1K7/v0gqEY6ilcxiVSL5jT+aAcs0noT4LMFvb993antq/
bAktScjGvlzt6n2QkdLkQ3DwsGYyMq9EEC6VgMpnQWvG2nh7FNf9zq43G1a6/pTc
eB0CTokp9PeIlz0Xlv9uYWHqv0l7O/VdgkfOzxnaGQIW/CJZQMSYHX5Hgf4lqIWP
9b30U5iLjMHlhl5NGisz7AXbxPw6huB44jVRD4dfBt74ndGE2TZpu7vLzgrnt8fT
UFhhwOELcTbS7cZR3DhK8qmfQoTtpwvnsNF5f7+9OlxTcixYnI4rCCjUKKnZGjEt
9wIDAQAB
REPLACE WITH THE CONTENTS OF public.pem
-----END PUBLIC KEY-----
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY: "qYAIWnRK52aBT5YQkBoMEw08j7j3+QIPZXS6+A8Su44="
# Protects stored provider, integration and LLM credentials. Changing it later
# leaves the existing ones unreadable, so set it before the first scan.
DJANGO_SECRETS_ENCRYPTION_KEY: ""
# openssl rand -base64 32
AUTH_SECRET: "CM9w3Nco2P1RdHaYmD+fmy2nJmSofusdHd4g7Z4KDG4="
AUTH_SECRET: ""
# Unfortunatelly, we need to duplicate the password in two different keys because the Neo4j Helm Chart expects the password in the NEO4J_AUTH key and the application expects it in the NEO4J_PASSWORD key.
NEO4J_PASSWORD: "prowler-password-fake"
+9 -3
View File
@@ -46,6 +46,9 @@ services:
ui:
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-stable}
restart: unless-stopped
environment:
# No default is shipped: abort `docker compose up` instead of crash-looping the UI
- AUTH_SECRET=${AUTH_SECRET:?generate it with openssl rand -base64 32 and set it in .env}
env_file:
- path: .env
required: false
@@ -75,7 +78,7 @@ services:
- path: .env
required: false
ports:
- "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}"
- "127.0.0.1:${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}"
healthcheck:
test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'"]
interval: 5s
@@ -92,7 +95,7 @@ services:
- path: .env
required: false
ports:
- "${VALKEY_PORT:-6379}:6379"
- "127.0.0.1:${VALKEY_PORT:-6379}:6379"
healthcheck:
test: ["CMD-SHELL", "sh -c 'valkey-cli ping'"]
interval: 10s
@@ -125,7 +128,7 @@ services:
# Networking
- "dbms.connector.bolt.listen_address=${NEO4J_DBMS_CONNECTOR_BOLT_LISTEN_ADDRESS:-0.0.0.0:7687}"
ports:
- ${NEO4J_PORT:-7687}:7687
- "127.0.0.1:${NEO4J_PORT:-7687}:7687"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "http://localhost:7474"]
interval: 10s
@@ -141,6 +144,7 @@ services:
- path: .env
required: false
volumes:
- ./_data/api:/home/prowler/.config/prowler-api
- "output:/tmp/prowler_api_output"
depends_on:
api:
@@ -159,6 +163,8 @@ services:
env_file:
- path: ./.env
required: false
volumes:
- ./_data/api:/home/prowler/.config/prowler-api
depends_on:
api:
condition: service_healthy
+1 -3
View File
@@ -80,8 +80,6 @@ Prowler Features are considered proper nouns. They are to be referenced without
Prowler ships two product families. Use these names exactly; the former names Prowler App (now Prowler Local Server) and Prowler Enterprise (now Prowler Private Cloud) must not appear in new writing. The only allowed former-name notes are on the Prowler Product Families page (`getting-started/products/index.mdx`) and in the site-wide banner, which document the mapping.
Web UI guides are written for Prowler Cloud. Steps that open the application link to `https://cloud.prowler.com/sign-in`, and the scope is stated once at the top of the section with the AppliesTo banner (see [AppliesTo Banner for Product Scope](#appliesto-banner-for-product-scope)). Do not offer Prowler Local Server as an inline alternative in a step list ("Go to Prowler Cloud or launch Prowler Local Server"). Mention Prowler Local Server inline only where its behavior differs, for example a feature that is not available or an environment variable that must be set.
Prowler Products:
* **Prowler Cloud**
@@ -575,7 +573,7 @@ The default covers Prowler Cloud, Prowler Private Cloud, and Prowler Local Serve
<AppliesTo products={["Prowler Cloud", "Prowler Private Cloud"]} />
```
Place it on its own line below the Version Badge when one is present, otherwise directly after the imports. On a page that also covers Prowler CLI, such as the provider getting-started guides, place it under the `## Prowler Cloud` heading instead, so it scopes only the web UI section.
Place it on its own line below the Version Badge when one is present, otherwise directly after the imports.
### Cloud Marker for Subscription Content
-205
View File
@@ -4,211 +4,6 @@ description: "New features and improvements in each Prowler release"
rss: true
---
<Update label="v5.47.0" description="October 29, 2026">
### 🧭 Add Provider — One Step to Connect Any Provider
The add-provider wizard follows the same model for every provider: one connect step, then the launch step. Adding an Azure subscription used to take six screens; now the account, its alias and its credentials are one form, and a single submit registers the account, stores the secret and tests the connection.
- A failed connection test shows inline, keeps the values and turns the action into **Retry connection** without registering the account twice.
- Access methods show as cards on the same step for AWS, GCP, Microsoft 365, GitHub, Alibaba Cloud and Cloudflare, and AWS, Azure and GCP switch to their organization flow through tabs.
- Users with the Manage Account permission can invite a teammate to finish the connection for every provider, not only AWS.
- **Ask Lighthouse AI** opens beside the wizard without closing it. It guides the connection from the Prowler docs by default and, when asked, connects the provider itself after confirming the provider, UID and credential field names, never echoing a secret back.
- Product tours and the guided onboarding sequence are removed; new tenants still land on the add-provider wizard.
### 🎯 Issues — Verdicts Explained in the List
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
- The issue list leads with the verdict, shows the attack path under each title and adds a **Why** column with the five evidence legs, so the reason for a verdict reads without opening the issue. The summary tiles become one line with the folded findings and the findings already covered by a control.
- A row opens in the side panel, next to Lighthouse AI, with the verdict, legs, remediation, triage and an **Attack path** tab with the issue graph, instead of expanding the issue inline.
- The menu entry is renamed to **Exploitability issues**.
### ☁️ Azure and AWS Checks
- `network_application_gateway_waf_enabled` and `network_application_gateway_http2_enabled` cover Azure Application Gateways and their WAF policies, mapped to CIS 5.0 and 6.0. Contributed by @BabafemiB. Thank you!
- `iam_subscription_owners_between_2_and_3` verifies that each Azure subscription has between two and three Owners, counting owners inherited from management groups, mapped to CIS 5.0 and 6.0. Contributed by @BabafemiB. Thank you!
- The AWS security group internet exposure checks treat CIDRs in one ingress rule that together cover `0.0.0.0/0` or `::/0`, such as `0.0.0.0/1` plus `128.0.0.0/1`, as open to the internet; such a rule used to pass `ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22` and its siblings. Contributed by @harshrajdebug. Thank you!
Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
### 🛠️ Improvements and Fixes
- Every MCP Server tool declares `readOnlyHint` and `destructiveHint`, so clients can tell a listing from a write when deciding whether to ask the user. Mutelist and mute rule writes are marked destructive because they hide findings for good.
- The cross-provider compliance tab keeps the frameworks that loaded, with a warning, when one catalog page or framework fails, instead of going blank.
- The NATO D32 compliance framework has its own icon on the compliance cards.
- Lighthouse AI replies no longer blink when the response finishes streaming, and the user avatar no longer requests the current page as an image, which rendered every page twice.
- The Invitations page uses **Status** instead of **State**, and the Invitations and Roles filters show capitalized options.
- **Prowler Cloud:** actions refused because the tenant is over its plan's usage limit, such as launching a scan, saving a schedule, adding a provider or triaging a finding, now link to the plans page.
- **Prowler Cloud:** creating API keys is a Prowler Cloud feature; on Local Server the button opens the upgrade dialog, and existing keys can still be edited and revoked.
- **Prowler Cloud:** issue evaluation after a scan reads the scan's findings through their indexes instead of a server-side cursor that took hours on large scans, so the evaluations no longer queue up behind each other.
- **Prowler Private Cloud:** Lighthouse AI can use Amazon Bedrock with only a region, relying on the installation's own AWS role, when the API allows it.
### 🔐 Security Updates
- `pymongo` upgraded to 4.18.3, `dnspython` to 2.9.0 and `oauthlib` to 4.0.0 in the SDK and the API container image, patching the BSON decoding and DNS compression pointer denial of service advisories and the OAuth device flow token disclosure. The API also moves `django-allauth` to 65.19.7, since earlier releases cap `oauthlib` below 4.
- `Authlib` upgraded to 1.9.1 and `dnspython` to 2.9.0 in the MCP Server container image, patching CVE-2026-96760 (critical), CVE-2026-114615, CVE-2026-114616 and CVE-2026-114605.
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @BabafemiB: Azure `network_application_gateway_waf_enabled`, `network_application_gateway_http2_enabled` and `iam_subscription_owners_between_2_and_3` checks ([#13160](https://github.com/prowler-cloud/prowler/pull/13160), [#13145](https://github.com/prowler-cloud/prowler/pull/13145))
- @harshrajdebug: AWS security group internet exposure fix for split CIDRs ([#13191](https://github.com/prowler-cloud/prowler/pull/13191))
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.47.0) for the complete list of changes.
</Update>
<Update label="v5.46.0" description="October 26, 2026">
### 🎯 Issues — Attack Paths with an Exploitability Verdict
<Note>
This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
</Note>
A scan returns a list of failed checks, and most of them are one problem seen from several angles. The new **Issues** page folds the failed findings of each account into attack paths and gives each one a verdict: `exploitable`, `likely`, `conditional`, `not_exploitable` or `unknown`. The verdict is drawn from five legs of evidence (reachability, access, target, controls and verification), each naming the checks and resources it was read from, so a reader can disagree with it instead of having to trust it. A control in force moves a verdict one rung down and the issue says which control did it; a check that did not run reads as `unknown`, never as safe.
- Patterns cover AWS, Azure, GCP and Microsoft 365 and are keyed on Prowler check IDs, so detection is never reimplemented. Six more AWS paths ship in this release: Lambda functions anyone can invoke, snapshots shared with every AWS account, instances that can leak their role's credentials, database ports open to the internet, stale or weakly protected IAM user credentials, and roles another AWS account can use.
- Each issue says what the path buys an attacker and where the attacker has to start, lists the compliance frameworks its findings put at risk, and can be sent to Jira as one grouped ticket.
- An attack path can be triaged with a status and notes from its detail. The decision is kept per account and pattern, so it survives the next scan, which marks it resolved when the path is gone and reopened when it comes back.
- The issue list opens with summary tiles and names the account of each issue, so two accounts with the same misconfiguration no longer read as one row. Each issue shows its risk factors, the triage status of every folded finding and an issue graph that starts internet-facing paths at the Internet node, drawn only where a reachability check failed on the resource, and has a page of its own to share.
- The Overview shows a verdict share card and the top issues with their risk factors, with counts scoped to the latest completed scan of each provider.
- **API:** `GET /api/v1/issues`, `GET /api/v1/issues/{id}` and `GET /api/v1/issues/overview`, with `filter[outcome__in]`, `filter[attacker_start__in]` and `filter[triage_status__in]`; triage lives at `/api/v1/issues/{id}/triage` and `/api/v1/issues/{id}/triage/notes`.
### 🐘 PHP — Coverage After the October Security Release
The PHP project shipped an emergency release on October 21 fixing two unauthenticated remote code execution flaws (CVE-2026-109618, CVE-2026-109619), with no fix for 8.1 and older branches. Four new checks cover how each cloud exposes the runtime, and the Azure runtime checks no longer fail every version newer than the configured one.
- **AWS:** `elasticbeanstalk_environment_php_platform_updated` flags PHP environments on branches without security fixes or on platform versions older than the one bundling the fixed release, `lightsail_instance_php_blueprint_updated` reports instances created from PHP blueprints for manual verification, and `wafv2_webacl_managed_rule_groups_enabled` verifies that a Web ACL references at least one managed rule group.
- **GCP:** new `appengine` service and `appengine_version_php_runtime_supported` check, flagging App Engine serving versions on PHP runtimes without security fixes.
- **Azure:** `app_ensure_php_version_is_latest`, `app_ensure_python_version_is_latest` and `app_ensure_java_version_is_latest` now pass any runtime at or above the configured minimum, and the default `php_latest_version` is raised from `8.2` to `8.5` in the SDK and in the scan configuration template of Prowler App.
The version thresholds are configurable; see the [configuration file documentation](https://docs.prowler.com/user-guide/cli/tutorials/configuration_file).
### 🧠 GCP — Vertex AI Workbench Coverage
A new Vertex AI service audits Workbench instances with five checks, all contributed by @rajeshnadiminti. Thank you!
- `vertexai_workbench_instance_public_ip_disabled` and `vertexai_workbench_instance_root_access_disabled`, mapped to PCI DSS 4.0.
- `vertexai_workbench_instance_terminal_access_disabled`, `vertexai_workbench_instance_file_download_disabled` and `vertexai_workbench_instance_auto_upgrade_enabled`, with the Workbench checks mapped to the CCC MLDE requirements.
### 🤖 AWS — Bedrock and Cognito Checks
- `bedrock_guardrail_sensitive_information_filter_enforced` flags guardrails whose PII entities or custom regexes take no action on the output path, so a detected match is reported in the trace and still delivered to the caller. Contributed by @tamg-aws. Thank you!
- `bedrock_model_invocation_logging_text_data_enabled` verifies that model invocation logging delivers prompt and response text rather than naming a destination while excluding request and response bodies. Contributed by @tamg-aws. Thank you!
- `cognito_user_pool_account_recovery_sms_not_preferred` verifies that user pools do not rank SMS first for account recovery. Contributed by @UTKARSH698. Thank you!
- `bedrock_api_key_no_long_term_credentials` now reports long-term API keys that never expire; IAM only returns `ExpirationDate` for keys created with an expiration period, so keys created without one produced no finding at all. Also contributed by @tamg-aws.
Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
### 📬 Google Workspace — Policy Precedence and Edition Context
- Checks now read the policy Google applies (the greatest `sortOrder`) instead of the last policy the Cloud Identity Policy API lists, which reported default values over admin-configured ones.
- Checks for Chat, Drive, Gmail, Calendar and Groups pass with the reason stated when the app is turned off for everyone and no organizational unit or group turns it back on. Drive sharing settings and the settings of existing groups keep being evaluated, because existing shares and groups stay reachable.
- Findings state when a feature is not sold on the tenant edition, such as Drive DLP rules, Google session control and the shared drive sharing settings on Business Starter. Reading editions needs the optional `apps.licensing` scope; see the [authentication documentation](https://docs.prowler.com/user-guide/providers/googleworkspace/authentication).
- The Gmail spoofing checks read each protection action from its own Policy API field: the inbound domain spoofing check could never pass and the similar domain names check reported the inbound action.
- `drive_publishing_files_disabled` passes when sharing outside the organization is limited to allowlisted domains or off, and `drive_desktop_access_disabled` remediation points to the Google Drive for desktop card in the Admin console.
### 🛠️ Improvements and Fixes
- Scan report generation parses each resource's metadata once per batch, sanitizes it once per finding and releases each batch before building the next one, so providers with very large resource metadata no longer exhaust the worker memory.
- Scan ingestion compares the serialized resource metadata with the stored value and serializes sets in a stable order, so unchanged resources no longer get their metadata, details and `updated_at` rewritten on every scan.
- The compliance report PDF cover table wraps every value, so long account IDs such as OCI tenancy OCIDs no longer overflow their cell.
- **Launch an initial scan now** is ticked by default on the provider and organization setup launch steps, so saving a schedule also runs a first scan unless the box is unticked.
- The SAML SSO integration card on the profile page is shown to roles with the Manage Account permission, matching the API requirement.
- **Prowler Cloud:** the scan-reports worker replaces each Celery child process after one report, so the memory a large report needs is returned instead of pushing the task towards an out-of-memory kill, and a container health check replaces a worker that stops consuming instead of leaving it in steady state.
- **Prowler Cloud:** persistent report, compliance and overview workers no longer shut themselves down when a task named like a scan hits its time limit or is revoked; the one-shot shutdown only applies to the ephemeral workers of the `scans` queue.
### 🔐 Security Updates
- Resource metadata attached to findings no longer carries the values that secrets checks scan (CloudFormation outputs, Lambda environment variables, EC2 user data and similar fields); they are replaced with `REDACTED` before reaching the Evidence tab, the API and the output files.
- SAML SSO configuration endpoints require the `manage_account` permission instead of `manage_integrations`, since the configured IdP controls who signs in and which role they receive.
- Attack Paths custom queries strip block comments before the blocked-operation check and drop APOC virtual nodes and relationships from results, so `LOAD CSV` can no longer be split with a comment nor its response returned under a forged provider label.
- `GET /resources/metadata`, `GET /resources/metadata/latest`, `GET /findings/metadata` and `GET /findings/metadata/latest` honour the provider groups of the caller role instead of aggregating every provider in the tenant.
- OpenStack `auth_url` values resolving to loopback, private or otherwise non-public hosts are rejected before the SDK connects, on both the connection test and the scan.
- `djangorestframework` upgraded to 3.18.3 in the API, patching the JSON parser charset and Accept header denial of service and the TokenProxy revocation bypass.
- `Pillow` upgraded to 13.0.0 in the API, patching a JPEG decoder heap overflow.
- `certifi` upgraded to 2026.10.19 in the SDK, the API and the MCP Server, dropping the emSign root CAs whose intermediate keys were compromised, and `uv` to 0.13.4 in the SDK and API images for the same reason.
- `aiohttp` upgraded to 3.14.5 with `yarl` 1.25.1 in the SDK and the API, patching the aiohttp resource exhaustion advisories.
- `black` upgraded to 26.10.1 and `graphql-core` to 3.2.12 in the SDK, patching CVE-2026-114134, CVE-2026-75507 and CVE-2026-75508, and `libbrotli1` to 1.1.0-2+deb13u1 in its container image, patching CVE-2026-111497.
- Trivy upgraded to 0.76.1 in the SDK and API images, patching the HTTP/2, TLS and Go 1.27.2 stdlib CVEs bundled in Trivy 0.75.0, and the SDK and API images no longer ship the MicrosoftTeams build SBOM that Trivy read as installed `zod` and `body-parser`.
- `next` upgraded to 16.4.1 in the UI, patching two unauthenticated remote code executions (Image Optimization with SVG files, GHSA-2464-fhr4-w9xw; `use cache` functions returning typed arrays, GHSA-673q-mjm6-r5xr), `@sentry/nextjs` to 10.75.2 for the middleware bypass through the tunnel route (GHSA-mchm-gjx6-7h55) and DOMPurify to 3.4.17 for the `SAFE_FOR_TEMPLATES` bypass (GHSA-cg2g-xh64-v4q7).
- Node.js upgraded to 24.22.1 LTS in the UI container image, patching the eight CVEs of the October 2026 security release that affect the 24.x line, including the Brotli decoder remote code execution (CVE-2026-111497); the base image moves to Alpine 3.24.
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @rajeshnadiminti: GCP Vertex AI service and its Workbench checks ([#13062](https://github.com/prowler-cloud/prowler/pull/13062), [#13077](https://github.com/prowler-cloud/prowler/pull/13077))
- @tamg-aws: AWS `bedrock_guardrail_sensitive_information_filter_enforced` and `bedrock_model_invocation_logging_text_data_enabled` checks and the `bedrock_api_key_no_long_term_credentials` fix ([#12663](https://github.com/prowler-cloud/prowler/pull/12663))
- @UTKARSH698: AWS `cognito_user_pool_account_recovery_sms_not_preferred` check ([#12251](https://github.com/prowler-cloud/prowler/pull/12251))
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.46.0) for the complete list of changes.
</Update>
<Update label="v5.45.0" description="October 15, 2026">
### 🤖 Azure — AI Services Coverage
A new Azure AI Services service audits the Azure AI and Azure OpenAI accounts of each subscription, with nine checks across network exposure, authentication, encryption, identity, logging and content safety, all contributed by @rajeshnadiminti. Thank you!
- `aiservices_account_public_network_access_disabled`, `aiservices_account_network_acls_default_action_deny`, `aiservices_account_uses_private_endpoint` and `aiservices_account_outbound_network_restricted` cover how the account can be reached and what it can reach.
- `aiservices_account_local_auth_disabled` and `aiservices_account_managed_identity_enabled` cover key-based access and the account's own identity.
- `aiservices_account_encrypted_with_cmk` and `aiservices_account_diagnostic_logging_enabled` cover customer-managed key encryption and diagnostic logs.
- `aiservices_deployment_content_filter_prompt_shield_enabled` verifies that model deployments have Prompt Shields enabled in their content filter.
### 🪪 Microsoft 365 — New Entra ID Checks
Five new Entra ID checks, all contributed by @kinomakino. Thank you!
- `entra_cross_tenant_access_default_outbound_restricted` verifies that the default cross-tenant access policy does not let every user reach every external application through B2B collaboration or B2B direct connect.
- `entra_policy_admin_self_service_password_reset_disabled` verifies that Self-Service Password Reset is disabled for administrators.
- `entra_policy_msol_powershell_blocked` verifies that the tenant authorization policy blocks the legacy MSOnline PowerShell module.
- `entra_app_registration_redirect_uris_secure` flags app registrations with HTTP, wildcard, malformed or `azurewebsites.net` redirect URIs.
- `entra_dynamic_group_member_of_rule_not_used` flags dynamic groups whose membership rule uses the `memberOf` operator, which Microsoft retires on November 3, 2026.
Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
### ☁️ OCI — Connect Tenancies Outside us-ashburn-1
OCI API key credentials now take a home region, and the credentials form in Prowler App requires it. Identity calls that validate the credentials and discover the subscribed regions used to go to `us-ashburn-1` regardless of the tenancy, so a tenancy not subscribed to that region could not connect. The home region is only used for that bootstrap; scans still audit every subscribed region.
Read more in the [Getting Started with OCI documentation](https://docs.prowler.com/user-guide/providers/oci/getting-started-oci).
### 🛠️ Improvements and Fixes
- **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, which timed out in GovCloud.
- API Gunicorn workers are recycled after 10,000 requests, plus up to 1,000 of jitter, to cap memory growth. Both values are configurable with `GUNICORN_MAX_REQUESTS` and `GUNICORN_MAX_REQUESTS_JITTER`.
- The first-login redirect to the Add Provider wizard is retried on the next load when the navigation was cut short, instead of being written off after a single attempt.
- The AWS connect step offers to invite a teammate when the user cannot access the account credentials.
- **Prowler Cloud:** the Slack channel picker reads the channels the Prowler app is in before the public ones, so invited private channels show up in large workspaces, and the partial list message tells the user to invite the app to a missing channel.
### 🔐 Security Updates
- Kubernetes kubeconfigs accept only inline credentials and known cluster fields. File-path fields, command-based authentication, `auth-provider` directives, proxy URLs and unknown keys are rejected by the API and the credentials form, and stored kubeconfigs are re-validated before each scan and connection test.
- IaC repository URLs that resolve to loopback, private or otherwise non-public hosts are rejected before the connection test and the scan clone, including every destination a redirect points to.
- CSV outputs prefix cells starting with `=`, `+`, `-`, `@`, tab or carriage return with a single quote so spreadsheet applications treat them as text, and the HTML report escapes the compliance column.
- `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.
- API error reports sent to Sentry no longer include user identity, IP addresses, cookies, headers or request bodies.
- PowerShell upgraded to 7.5.12 in the SDK and API container images, bundling .NET runtime 9.0.21.
- Python base images upgraded to 3.12.15 in the SDK and API and to 3.13.16 in the MCP Server, and Trivy to 0.75.0, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy.
- `django` upgraded to 5.2.17 in the API, patching CVE-2026-15307.
- `pyjwt` upgraded to 2.14.0 in the SDK, the API and the MCP Server, patching GHSA-ffc3-869f-jxw9.
- `pymongo` upgraded to 4.18.2 and `urllib3` to 2.8.0 in the SDK and the API, patching CVE-2026-96748, CVE-2026-96749, CVE-2026-97687 and CVE-2026-97689.
- `dulwich` upgraded to 1.2.9 in the SDK and the API, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx.
- `next` upgraded to 16.3.8 in the UI, patching a remote code execution in `next/og` (GHSA-vcvr-r3jv-pc5j) and a server-side request forgery in Image Optimization (GHSA-cjq9-62q9-8jv4).
- `sharp` upgraded to 0.35.5 and `proxy-addr` to 2.0.8 in the UI, patching GHSA-wq5f-xc86-pv6w and GHSA-jqcg-44mw-7w3h.
### 🙌 External Contributors
Thank you to our community contributors for this release!
- @rajeshnadiminti: Azure AI Services service and its checks ([#12925](https://github.com/prowler-cloud/prowler/pull/12925), [#12926](https://github.com/prowler-cloud/prowler/pull/12926), [#12997](https://github.com/prowler-cloud/prowler/pull/12997))
- @kinomakino: Microsoft 365 `entra_cross_tenant_access_default_outbound_restricted` ([#12991](https://github.com/prowler-cloud/prowler/pull/12991)), `entra_policy_admin_self_service_password_reset_disabled` ([#12990](https://github.com/prowler-cloud/prowler/pull/12990)), `entra_policy_msol_powershell_blocked` ([#13004](https://github.com/prowler-cloud/prowler/pull/13004)), `entra_app_registration_redirect_uris_secure` ([#12993](https://github.com/prowler-cloud/prowler/pull/12993)) and `entra_dynamic_group_member_of_rule_not_used` ([#13003](https://github.com/prowler-cloud/prowler/pull/13003)) checks
See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.45.0) for the complete list of changes.
</Update>
<Update label="v5.44.0" description="September 29, 2026">
### 🔁 Findings — Re-check a Resource with a Partial Scan
+1 -1
View File
@@ -3251,7 +3251,7 @@ If your provider has specific fields, add them to the finding transformation:
**File:** `prowler/lib/outputs/finding.py`
```python
def transform_api_finding(cls, finding, provider, metadata_cache=None) -> "Finding":
def transform_api_finding(cls, finding, provider) -> "Finding":
# ... existing code ...
# Your provider specific field
+2
View File
@@ -55,6 +55,8 @@ The Prowler CLI image is also available on AWS Public ECR: [`public.ecr.aws/prow
```console
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
# AUTH_SECRET is not shipped: set it in .env before the first start
perl -pi -e "s|^AUTH_SECRET=.*|AUTH_SECRET=\"$(openssl rand -base64 32)\"|" .env
docker compose up -d
```
@@ -2,15 +2,11 @@
title: "Installation"
---
<Tip>
[Prowler Cloud](https://cloud.prowler.com/sign-up) runs the same application with no installation or maintenance. Follow the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) to start scanning right away.
</Tip>
### Installation
Prowler Local Server offers flexible installation methods tailored to various environments.
Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detailed usage instructions, which apply to Prowler Local Server as well.
Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detailed usage instructions.
<Warning>
Prowler configuration is based on `.env` files. Every version of Prowler can have differences on that file, so, please, use the file that corresponds with that version or repository branch or tag.
@@ -30,6 +26,8 @@ Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detail
curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/docker-compose.yml"
# Environment variables can be customized in the .env file. Using default values in production environments is not recommended.
curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/.env"
# AUTH_SECRET is not shipped: set it in .env before the first start
perl -pi -e "s|^AUTH_SECRET=.*|AUTH_SECRET=\"$(openssl rand -base64 32)\"|" .env
docker compose up -d
```
@@ -38,12 +36,17 @@ Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detail
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/$VERSION/docker-compose.yml" -OutFile "docker-compose.yml"
# Environment variables can be customized in the .env file. Using default values in production environments is not recommended.
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/$VERSION/.env" -OutFile ".env"
# AUTH_SECRET is not shipped: set it in .env before the first start
$AuthSecret = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
(Get-Content .env) -replace '^AUTH_SECRET=.*', "AUTH_SECRET=`"$AuthSecret`"" | Set-Content .env
docker compose up -d
```
</CodeGroup>
<Callout icon="lock" iconType="regular" color="#e74c3c">
For a secure setup, the API auto-generates a unique key pair, `DJANGO_TOKEN_SIGNING_KEY` and `DJANGO_TOKEN_VERIFYING_KEY`, and stores it in `~/.config/prowler-api` (non-container) or the bound Docker volume in `_data/api` (container). Never commit or reuse static/default keys. To rotate keys, delete the stored key files and restart the API.
The API also generates `DJANGO_SECRETS_ENCRYPTION_KEY` on the first start, stored next to the key pair as `secrets_encryption.key`. Unlike the key pair, this one cannot be rotated by deleting the file: it protects every stored credential. See [Secrets encryption key](#secrets-encryption-key).
</Callout>
</Tab>
@@ -132,8 +135,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.47.0"
PROWLER_API_VERSION="5.47.0"
PROWLER_UI_VERSION="5.44.0"
PROWLER_API_VERSION="5.44.0"
```
<Note>
@@ -152,9 +155,19 @@ The `--policy always` flag ensures that Docker pulls the latest images even if t
<Note>
**What Gets Preserved During Upgrade**
Everything is preserved, nothing will be deleted after the update.
Everything is preserved, nothing will be deleted after the update. The `_data/` directory holds the database, the graph store and the keys under `_data/api`, including `secrets_encryption.key`: never remove it when upgrading.
</Note>
### Secrets encryption key
`DJANGO_SECRETS_ENCRYPTION_KEY` is the symmetric key that encrypts every cloud provider credential, integration credential and LLM key stored by Prowler. When the variable is empty, the API generates the key on the first start and persists it as `_data/api/secrets_encryption.key` (`~/.config/prowler-api/secrets_encryption.key` outside a container), where the `api`, `worker` and `worker-beat` containers read it. To manage it yourself, set the variable to the output of `openssl rand -base64 32` before the first start.
<Warning>
Keep this key across upgrades and back it up together with the database. Rotating it means re-encrypting every stored credential: Prowler does not do that automatically, so setting a new key leaves the existing credentials unreadable and they have to be entered again.
</Warning>
Older `.env` files shipped a working value for this variable, so every installation that kept it shares the same public key. The API now refuses to start with it and logs `DJANGO_SECRETS_ENCRYPTION_KEY is set to a value that was published in the Prowler repository`. To recover, clear the variable in `.env` (or set a freshly generated one), restart the stack and enter the provider, integration and LLM credentials again; they were encrypted with a public key and must be considered exposed.
### Troubleshooting Installation Issues
If containers don't start, check logs for errors:
@@ -2,7 +2,7 @@
title: 'Overview'
---
Prowler Local Server is a self-hosted web application that simplifies running Prowler. It is the open source counterpart of [Prowler Cloud](/getting-started/products/prowler-cloud), which runs the same application as a managed service. It provides:
Prowler Local Server is a self-hosted web application that simplifies running Prowler. It provides:
- **User-friendly interface** for configuring and executing scans
- Dashboard to **view results** and manage **security findings**
@@ -2,337 +2,54 @@
title: "AWS Marketplace"
---
This page explains how to subscribe to **Prowler Cloud** through the **AWS Marketplace**, how to activate the subscription in Prowler Cloud, how billing works, and how to fix the errors you may hit on the way.
This section contains the instructions to subscribe to **Prowler Cloud** through the **AWS Marketplace**.
## Plans
## How to Subscribe
Prowler Cloud has two listings in the AWS Marketplace. Both give you the same product. They differ in how you commit and how you pay.
To get to the **Prowler Cloud** product listing in the AWS Marketplace, and click the `View purchase options` button:
A **provider** is a cloud account you connect to Prowler Cloud: an AWS account, an Azure subscription, a GCP project, a Kubernetes cluster, and so on.
1. Use this link to be taken directly to the [Prowler Cloud Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-6ochhig5kxpok):
| | Monthly (pay as you go) | Annual (billed annually) |
| --- | --- | --- |
| Listing | [Prowler Cloud $99/provider per month (Pay as You Go)](https://aws.amazon.com/marketplace/pp/prodview-nmbvelz746oyk) | [Prowler Cloud $79/provider per month (Billed Annually)](https://aws.amazon.com/marketplace/pp/prodview-wwehsmzui25ri) |
| Billing | Monthly, per connected provider | Once, at checkout, for the full 12 months |
| Commitment | None. Cancel anytime. | 12 months for the number of providers you choose during checkout on AWS. |
| Resources beyond the included amount | Billed per resource | Billed per resource, at the annual rate |
| Providers beyond your commitment | Not applicable | Billed monthly at the pay-as-you-go rate |
![](/images/aws-marketplace/marketplace-listing.png)
For prices, included resources and per-resource rates, see the [Prowler Cloud pricing page](https://prowler.com/pricing/).
2. Then, scroll down to the "Purchase Details" section and click the `Subscribe` button:
## Before You Subscribe
![](/images/aws-marketplace/marketplace-subscribe.png)
- **One AWS account pays for the whole organization.** Subscribe from a single AWS account. You then connect as many providers as you need, from any cloud, inside Prowler Cloud. You do not subscribe again for each AWS account you scan.
- **Decide which Prowler Cloud organization will pay.** Activation links the AWS purchase to the organization you are signed in to. If you belong to several organizations, switch to the right one before you activate. See [Activate the Subscription in Prowler Cloud](#activate-the-subscription-in-prowler-cloud).
- **Annual plan: decide the provider count first.** AWS asks for it during checkout, and AWS charges the full year at that moment. Providers you add beyond that count are billed monthly at the pay-as-you-go rate. AWS may not let you cancel an annual contract yourself from the console. See [Cancel the Subscription](#cancel-the-subscription) before you commit.
- **Cancel any card-billed subscription first.** An organization already billed by credit card cannot switch to AWS Marketplace billing until that subscription is cancelled. This prevents double charges.
## Set Up Your Account
## Subscribe on AWS Marketplace
After you have subscribed to the **Prowler Cloud** product, you will need to set up your **Prowler Cloud** account:
<Steps>
<Step title="Open the listing">
Sign in to the AWS account that will pay, open the listing for the plan you want, and click **View purchase options**.
1. Click the `Set up your account` button:
<Tabs>
<Tab title="Monthly">
Open [Prowler Cloud $99/provider per month (Pay as You Go)](https://aws.amazon.com/marketplace/pp/prodview-nmbvelz746oyk).
![](/images/aws-marketplace/marketplace-message.png)
<Frame>
<img src="/images/aws-marketplace/listing-monthly.png" alt="Monthly Prowler Cloud listing on AWS Marketplace with the View purchase options button highlighted" />
</Frame>
</Tab>
<Tab title="Annual">
Open [Prowler Cloud $79/provider per month (Billed Annually)](https://aws.amazon.com/marketplace/pp/prodview-wwehsmzui25ri).
2. You will be redirected to **Prowler Cloud Sign In** page. You can sign in with an existing account or sign up with a new account:
<Frame>
<img src="/images/aws-marketplace/listing-annual.png" alt="Annual Prowler Cloud listing on AWS Marketplace with the View purchase options button highlighted" />
</Frame>
</Tab>
</Tabs>
</Step>
![](/images/aws-marketplace/marketplace-sign-up.png)
<Step title="Review the purchase and subscribe">
<Tabs>
<Tab title="Monthly">
Scroll to **Purchase details** and click **Subscribe**. There is no upfront charge. AWS bills your usage at the end of each month.
3. Once you have successfully authenticated, you should be automatically redirected to the **Prowler Cloud** [Billing](https://cloud.prowler.com/billing) page where you should now see that your account has the `AWS Marketplace Subscription` badge.
<Frame>
<img src="/images/aws-marketplace/purchase-monthly.png" alt="Purchase details for the monthly listing with the Subscribe button highlighted" />
</Frame>
</Tab>
<Tab title="Annual">
Under **Pricing details**, select the **providers** dimension and enter the **Number of units**. One unit is one provider for the 12-month term. Review the **Contract total**, then click **Subscribe** at the bottom of the page. AWS charges the full year now.
![](/images/aws-marketplace/marketplace-my-account.png)
<Frame>
<img src="/images/aws-marketplace/purchase-annual.png" alt="Purchase options for the annual listing showing the contract details, the providers dimension and the Number of units table" />
</Frame>
If you have any issues signing up, please contact us at support@prowler.com.
**Auto-renewal** is on by default. Leave it on to have AWS renew the contract for another 12 months at the end date, or choose **Do not automatically renew this contract**. You can change this from **Manage subscriptions** any time before the renewal date.
</Tab>
</Tabs>
</Step>
<Step title="Click Set up your account">
AWS confirms the purchase and shows a **Set up your account** button. Click it. AWS sends your browser to Prowler Cloud with a setup link.
## Billing
<Frame>
<img src="/images/aws-marketplace/set-up-your-account.png" alt="AWS Marketplace purchase confirmation banner with the Set up your account button" />
</Frame>
You will be charged monthly based on resources scanned and monitored depending on usage in **Prowler Cloud**. For more information on billing, please see the [Prowler Cloud Pricing FAQ](https://prowler.com/pricing/).
<Note>
The setup link expires quickly: AWS documents roughly one hour, and never more than four. If the link expires, you close the tab, or need to start over, return to [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) in the AWS Marketplace console, open **Prowler Cloud**, and click **Set up your account** again to get a fresh link.
</Note>
</Step>
</Steps>
**Note:** Your **Prowler Cloud** bills can be seen at [AWS Billing](https://us-east-1.console.aws.amazon.com/billing/home#/bills).
## Activate the Subscription in Prowler Cloud
## Subscription
Nothing is linked until you confirm it in Prowler Cloud. The purchase is safe on the AWS side while you complete these steps.
If you subscribe to Prowler Cloud through the AWS Marketplace it is not necessary to subscribe from different AWS accounts to use Prowler Cloud for those accounts.
<Steps>
<Step title="Sign in or create your account">
Prowler Cloud shows the sign-in page with a banner that reads "To finish setting up your AWS Marketplace subscription, sign in or register if you don't have an account yet."
<Frame>
<img src="/images/aws-marketplace/sign-in-marketplace.png" alt="Prowler Cloud sign-in page with the AWS Marketplace banner" />
</Frame>
If you do not have an account yet, click **Sign up**. The purchase carries over once you sign in with the new account.
<Frame>
<img src="/images/aws-marketplace/sign-up-marketplace.png" alt="Prowler Cloud sign-up page with the AWS Marketplace banner" />
</Frame>
</Step>
<Step title="Check the organization and activate">
The billing page shows a card titled **Activate your AWS Marketplace subscription**. It names the organization the purchase will be linked to.
<Frame>
<img src="/images/aws-marketplace/activate-confirm.png" alt="Activation card naming the organization, with the Activate subscription button and the Continue to billing without activating link" />
</Frame>
<Warning>
Read the organization name before you click. Activation is a billing decision: charges for that organization go to the AWS account you subscribed with. If it is not the organization you want to pay for, switch organizations first, then come back to the billing page.
</Warning>
Click **Activate subscription**. Prowler Cloud links the purchase and activates the subscription. Two short status messages appear while it works: "Linking your AWS Marketplace purchase…" and "Activating your subscription…".
<Frame>
<img src="/images/aws-marketplace/activate-working.png" alt="Activation card showing the Activating your subscription progress message" />
</Frame>
**Continue to billing without activating** discards the setup link and leaves the organization unchanged. Use it only if you landed in the wrong organization. To activate later, get a fresh link from the AWS Marketplace console as described above.
</Step>
<Step title="Confirm the subscription is active">
The billing page now states that you subscribed through AWS Marketplace, shows the AWS account that pays, and links to your subscription in the AWS Marketplace console.
<Frame>
<img src="/images/aws-marketplace/billing-subscribed.png" alt="Prowler Cloud billing page for an AWS Marketplace subscription, showing the paying AWS account and the link to the AWS Marketplace console" />
</Frame>
There is no payment method, invoice history, or cancel button on this page. AWS handles all of that.
</Step>
</Steps>
If something goes wrong during activation, see [Activation errors](#activation-errors).
## After Activation
- Add providers from the **Providers** page in Prowler Cloud. Every provider you add is covered by the same AWS subscription, whatever cloud it belongs to.
- All charges come from AWS. Find them under **Amazon Web Services, Inc. - Marketplace** in [AWS Billing](https://console.aws.amazon.com/billing/home#/bills).
<Frame>
<img src="/images/aws-marketplace/marketplace-billing.png" alt="AWS Billing page showing the Amazon Web Services, Inc. - Marketplace line" />
</Frame>
### Monthly
AWS bills you at the end of each month for the providers connected and the resources scanned in Prowler Cloud during that month.
### Annual
AWS charges the full 12-month subscription once, at checkout. The providers covered by your commitment cost nothing more during the year. Your monthly AWS bill only includes usage beyond the commitment: extra providers at the pay-as-you-go rate, and resources beyond the included amount at the annual plan's per-resource rate.
If auto-renewal is on, AWS renews the contract for another 12 months at the end date and charges the new term the same way. Turn it off from **Manage subscriptions** before the renewal date if you do not want that.
## Cancel the Subscription
There is no cancel button in Prowler Cloud for AWS Marketplace subscriptions. How you cancel depends on the plan, following the [AWS Marketplace cancellation rules](https://docs.aws.amazon.com/marketplace/latest/buyerguide/cancel-subscription.html).
### Monthly
Cancel from the AWS Marketplace console at any time:
1. Open [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) and open **Prowler Cloud**.
2. Under **Active agreements**, select the agreement and click **View details**.
3. Open the **Actions** menu and choose **Cancel subscription**.
4. Type `confirm` and choose **Yes, cancel subscription**.
<Frame>
<img src="/images/aws-marketplace/aws-manage-subscriptions.png" alt="Prowler Cloud agreement page in the AWS Marketplace console with the Actions menu open on Cancel subscription" />
</Frame>
Cancelling stops future charges. Usage accrued before the cancellation still appears on your next AWS bill.
### Annual
Open the agreement from [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) first. If the **Actions** menu shows **Cancel subscription**, follow the monthly steps above. If it does not, AWS does not allow self-service cancellation for that contract:
- **Within 48 hours of purchase**, contact AWS Customer Service. AWS can cancel the contract and refund the full amount, but only if you ask for the refund.
- **After 48 hours**, contact [support@prowler.com](mailto:support@prowler.com). Prowler starts the cancellation request through AWS Marketplace and you approve it in the AWS Marketplace console. A refund is not automatic. Ask for it when you request the cancellation.
See [Refunds and cancellations in AWS Marketplace](https://docs.aws.amazon.com/marketplace/latest/buyerguide/buyer-refunds.html) for the AWS side of the process.
### What happens after cancellation
Once AWS ends the subscription, Prowler Cloud pauses scans for the organization. Providers, findings and scan history stay readable. A **Subscription required** card in the sidebar, labeled **Subscription cancelled**, explains it: "Scans are paused and your assessment data remains available. Subscribe again to continue scanning." The billing page shows the plan selection page.
<Frame>
<img src="/images/aws-marketplace/billing-cancelled.png" alt="Prowler Cloud after an AWS Marketplace cancellation, showing the Subscription cancelled card in the sidebar and the plan selection page" />
</Frame>
To subscribe again from the same AWS account into the same Prowler Cloud organization, subscribe from the listing and activate as before.
<Warning>
Deleting your Prowler Cloud organization does not cancel the AWS subscription. AWS keeps charging the agreement until you cancel it as described above.
</Warning>
In Prowler Cloud you only need to subscribe from one of your AWS accounts through the AWS Marketplace and add multiple provider accounts once you are in the Prowler Cloud console. We will send usage metrics to the AWS Marketplace regardless of the number of accounts you add in our platform, so the AWS Marketplace will bill you based on those usage metrics.
## Troubleshooting
### Activation errors
Each error appears on the Prowler Cloud billing page with the title below. Your AWS purchase is never lost by any of them. **Continue to billing** closes the message without activating.
<AccordionGroup>
<Accordion title="Your AWS Marketplace link has expired">
AWS no longer accepts the setup link, usually because it timed out. AWS documents roughly one hour of validity, and never more than four.
Open **Manage subscriptions** in the AWS Marketplace console, open **Prowler Cloud**, and click **Set up your account** to get a fresh link.
<Frame>
<img src="/images/aws-marketplace/error-token-expired.png" alt="Error notice: Your AWS Marketplace link has expired" />
</Frame>
</Accordion>
<Accordion title="This AWS Marketplace product is not supported yet">
The purchase went through on AWS, but this listing is not enabled for activation in Prowler Cloud. You see this when you subscribed to an earlier Prowler Cloud listing instead of one of the [two current plans](#plans).
Contact [support@prowler.com](mailto:support@prowler.com) with the AWS account you subscribed with. Click **Try again** once support confirms.
<Frame>
<img src="/images/aws-marketplace/error-product-not-supported.png" alt="Error notice: This AWS Marketplace product is not supported yet" />
</Frame>
</Accordion>
<Accordion title="This AWS subscription is already linked">
The purchase cannot be linked to the organization you are signed in to. The message shows the reason reported for this purchase:
- **"This AWS Marketplace subscription is no longer active."** The subscription was cancelled on AWS. Subscribe again from the listing. If you see this seconds after a new purchase, AWS may still be registering it: wait a minute, go back to **Manage subscriptions**, and click **Set up your account** again.
- **"This AWS Marketplace purchase is already linked to another Prowler account."** AWS identifies a purchase by AWS account and product, so one AWS account backs one Prowler Cloud organization per plan. Sign in to the organization that already holds it, or subscribe from a different AWS account.
- **"This account is already linked to another active AWS Marketplace purchase."** This organization already has an active AWS Marketplace subscription. Cancel that subscription in the AWS Marketplace console before activating a new one.
If none of these applies, contact [support@prowler.com](mailto:support@prowler.com).
<Frame>
<img src="/images/aws-marketplace/error-agreement-in-use.png" alt="Error notice: This AWS subscription is already linked, with the reason shown below the message" />
</Frame>
</Accordion>
<Accordion title="Your purchase is still being linked">
Another request is linking the same purchase, usually a second click or a second tab. Wait a moment and click **Try again**.
<Frame>
<img src="/images/aws-marketplace/error-link-in-progress.png" alt="Error notice: Your purchase is still being linked" />
</Frame>
</Accordion>
<Accordion title="You already have an active subscription">
This organization is billed by credit card. Cancel that subscription first, so you are never charged twice. Then open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** again.
<Frame>
<img src="/images/aws-marketplace/error-billing-provider-conflict.png" alt="Error notice: You already have an active subscription" />
</Frame>
</Accordion>
<Accordion title="This AWS purchase is billed under another organization">
AWS Marketplace billing for this AWS account and plan is already set up in a different Prowler Cloud organization. This also happens after you delete the organization that held the subscription and subscribe again from a new one.
Sign in to the organization that holds the billing and restart the setup from **Manage subscriptions** in the AWS Marketplace console. If that organization no longer exists, contact [support@prowler.com](mailto:support@prowler.com) so they can release the billing link.
</Accordion>
<Accordion title="We could not find your AWS purchase">
Prowler Cloud received no purchase to activate. Open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** to start again.
<Frame>
<img src="/images/aws-marketplace/error-no-agreement.png" alt="Error notice: We could not find your AWS purchase" />
</Frame>
</Accordion>
<Accordion title="We could not reach AWS Marketplace">
A temporary problem talking to AWS. Click **Try again**. If it keeps failing, contact [support@prowler.com](mailto:support@prowler.com) and include the detail text shown under the message.
<Frame>
<img src="/images/aws-marketplace/error-resolve-failed.png" alt="Error notice: We could not reach AWS Marketplace" />
</Frame>
</Accordion>
<Accordion title="We could not activate your subscription">
The purchase was linked but the activation did not complete. Click **Try again**. If it keeps failing, contact [support@prowler.com](mailto:support@prowler.com) and include the detail text shown under the message.
<Frame>
<img src="/images/aws-marketplace/error-subscribe-failed.png" alt="Error notice: We could not activate your subscription" />
</Frame>
</Accordion>
<Accordion title="Please sign in again">
Your Prowler Cloud session expired before the activation finished. Click **Sign in again** and pick up where you left off. The setup link stays valid during its expiry window.
<Frame>
<img src="/images/aws-marketplace/error-unauthorized.png" alt="Error notice: Please sign in again" />
</Frame>
</Accordion>
<Accordion title="We could not read your AWS purchase">
The setup link arrived damaged. Open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** to get a fresh one.
<Frame>
<img src="/images/aws-marketplace/error-invalid-input.png" alt="Error notice: We could not read your AWS purchase" />
</Frame>
</Accordion>
</AccordionGroup>
### Errors before the sign-in page
If the redirect from AWS itself fails, Prowler Cloud shows a plain page instead of the app. Each page links back to Prowler Cloud billing and to your AWS Marketplace subscriptions.
<AccordionGroup>
<Accordion title="We couldn't verify this request">
The setup page only accepts the redirect from AWS Marketplace. You reach it by opening the setup URL directly or from a bookmark. Start again from **Set up your account** in the AWS Marketplace console.
<Frame>
<img src="/images/aws-marketplace/fulfillment-error-verify.png" alt="Error page: We couldn't verify this request" />
</Frame>
</Accordion>
<Accordion title="We couldn't read your AWS purchase">
The redirect arrived without a valid setup token. Start again from **Set up your account** in the AWS Marketplace console.
<Frame>
<img src="/images/aws-marketplace/fulfillment-error-read.png" alt="Error page: We couldn't read your AWS purchase" />
</Frame>
</Accordion>
<Accordion title="Something went wrong">
Prowler Cloud could not process the redirect. Your purchase is safe. Wait a moment, then start again from **Set up your account** in the AWS Marketplace console. Contact [support@prowler.com](mailto:support@prowler.com) if it happens again.
<Frame>
<img src="/images/aws-marketplace/fulfillment-error-generic.png" alt="Error page: Something went wrong" />
</Frame>
</Accordion>
</AccordionGroup>
### SEPA payment method issues
### SEPA Payment Method Issues
If AWS Marketplace notifies that payment failed due to an issue with the payment method, this typically occurs when a SEPA bank account is set as the default payment method. AWS Marketplace does not support SEPA bank accounts for product subscriptions, even when the account includes valid alternative payment methods. This is because AWS Marketplace invoices are issued by AWS Inc., a US entity. SEPA accounts do not recognize these invoices as valid, causing subscription failures.
@@ -356,14 +73,10 @@ The AWS Marketplace team acknowledges this limitation: "We apologize for these a
</Note>
### Credit and debit card storage restrictions (AISPL customers in India)
### Credit and Debit Card Storage Restrictions (AISPL Customers in India)
AWS Marketplace no longer supports payments using credit or debit cards stored on file for Amazon Internet Services Private Limited (AISPL) customers. This restriction stems from Reserve Bank of India (RBI) regulations regarding payment aggregators, which prohibit the storage of card data. As explained in [this AWS blog post](https://aws.amazon.com/blogs/awsmarketplace/restriction-on-credit-and-debit-card-purchases-for-aispl-customers-using-aws-marketplace/):
> AWS Marketplace can no longer support payments using credit or debit cards stored on file. [The Reserve Bank of India (RBI) has issued a notice regarding regulation of payment aggregators](https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=11822&Mode=0), which restricts the storage of card data. If you are currently using credit or debit card as your default payment instrument, your ability to use AWS Marketplace products will be restricted. However, you can switch your default payment instrument to Pay By Invoice to avoid disruption or restore your original experience.
To maintain uninterrupted access to AWS Marketplace products, change the default payment instrument from stored card data to Pay By Invoice billing.
## Support
If you get stuck at any step, contact [support@prowler.com](mailto:support@prowler.com) and include the AWS account you subscribed with and the exact error title you see.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Loaded 100 of 1137 files, more files were not shown because too many files have changed in this diff. Show more