Compare commits

..
51 Commits
Author SHA1 Message Date
Prowler Botandprowler-bot f1bb376e66 chore(release): Bump versions to v5.46.1 (#13164)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-10-26 20:54:48 +01:00
Prowler Botandprowler-bot 91b86777ce chore(api): Update prowler dependency to v5.46 for release 5.46.0 (#13163)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-10-26 18:56:37 +01:00
Prowler Botandprowler-bot fdb1bd6193 chore(changelog): v5.46.0 (#13162)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-10-26 18:46:03 +01:00
Rubén De la Torre Vicoandalejandrobailo 41a2bf605a feat(ui): bring the issues UI closer to the original prototype (#13154)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-10-26 17:24:52 +01:00
Rubén De la Torre Vicoandalejandrobailo 63b8c8969c feat(ui): show the compliance an issue puts at risk (#13153)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-10-26 16:44:04 +01:00
Rubén De la Torre Vicoandalejandrobailo 1ae3856de0 feat(ui): show what each attack path buys and where it starts (#13152)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-10-26 15:59:38 +01:00
40758818c3 feat(aws): Update regions for AWS services (#13156)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2026-10-26 15:10:49 +01:00
Rubén De la Torre Vicoandalejandrobailo 83152ae1a6 feat(ui): triage issues and send them to Jira (#13151)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-10-26 15:09:05 +01:00
Pedro Martín 3bbd551f0f fix(openstack): block SSRF via auth_url in connection test (#12962) 2026-10-26 13:37:46 +01:00
Pedro Martín be01a97789 fix(outputs): parse resource metadata once per batch (#13149) 2026-10-26 12:57:52 +01:00
Alejandro Bailo 7ecb6aaefd fix(ui): match the "Save and launch scan" footer in the provider E2E flows (#13133) 2026-10-26 12:53:36 +01:00
Pedro Martín 75f81cc751 ci(ui): pin Playwright jobs to ubuntu-24.04 (#13158) 2026-10-26 12:32:58 +01:00
Pedro Martín c724840e6a fix(api): compare serialized metadata on scan ingestion (#13148) 2026-10-26 12:13:02 +01:00
Pedro Martín b52b8d3678 fix(api): bump djangorestframework to 3.18.3 (#13157) 2026-10-26 11:56:25 +01:00
Alejandro Bailo c533b47aeb fix: patch critical UI advisories and SDK container CVEs (#13135) 2026-10-26 11:51:31 +01:00
Pedro MartínandRubén De la Torre Vico 669df91ac9 feat(checks): PHP runtime coverage after the October security release (#13127)
Co-authored-by: Rubén De la Torre Vico <98956809+puchy22@users.noreply.github.com>
2026-10-23 12:49:36 +02:00
Alejandro Bailo 35d25a7218 feat(ui): limit locked tenants to a ten-row findings preview (#13130) 2026-10-23 10:58:15 +02:00
Pepe Fagoaga 819f5fd7a0 chore(googleworkspace): name the Policy API reducer the precedence rule implements (#13125) 2026-10-23 08:38:13 +02:00
Pedro Martín 7d71220a25 fix(sdk): redact secret fields in resource metadata (#13129) 2026-10-23 08:37:27 +02:00
Rubén De la Torre Vico 69b80f6a3f chore(azure): raise App Service PHP latest version default to 8.5 (#13128) 2026-10-22 15:33:06 +02:00
Alejandro Bailo f26b9776d4 fix(ui): bump Node to 24.22.1 to patch the October 2026 security release (#13126) 2026-10-22 14:28:46 +02:00
Pepe Fagoaga 54f2400aee fix(googleworkspace): map Gmail spoofing actions to the right fields (#13114) 2026-10-22 08:26:11 +02:00
Pepe Fagoaga 8c5357b087 feat(googleworkspace): explain findings gated by the Workspace edition (#13113) 2026-10-22 08:25:52 +02:00
Pedro Martín 13202b8a70 fix(api): require manage_account for SAML configuration (#13121) 2026-10-22 08:24:22 +02:00
Utkarsh Batham 680acf8b31 feat(cognito): add cognito_user_pool_account_recovery_sms_not_preferred check (#12251) 2026-10-22 01:32:19 +02:00
Jonathan Nguyen 1468596b61 feat(bedrock): assess invocation log content and guardrail output filtering (#12663) 2026-10-22 01:25:39 +02:00
Pepe Fagoaga 02c8881378 fix(googleworkspace): honor the sharing mode in Drive publishing check (#13115) 2026-10-22 01:11:14 +02:00
Pepe Fagoaga eae17a5e17 feat(googleworkspace): pass app checks when the service is off for everyone (#13117) 2026-10-22 01:07:19 +02:00
Pedro Martín a59eae7b24 fix(api): harden Attack Paths custom query sanitizer (#13120) 2026-10-22 01:05:45 +02:00
Alejandro Bailo 9a89dc4a4e feat(ui): add outlined error badge variant (#13111) 2026-10-21 22:38:15 +02:00
Pedro Martín 1e2f8f3cf7 fix(container): patch Pillow, certifi, uv, aiohttp and Trivy CVEs (#13107) 2026-10-21 16:26:59 +02:00
Pepe Fagoaga d9ace56e43 docs(googleworkspace): link required APIs, handle key creation policy (#13108) 2026-10-21 15:52:48 +02:00
Pedro Martín c0ce556ea1 fix(ci): allow the Python download CDN in blocked jobs (#13109) 2026-10-21 15:45:43 +02:00
Alejandro Bailo 34e0af33c2 feat(ui): launch the initial scan by default after provider setup (#13104) 2026-10-21 15:42:41 +02:00
Toni de la Fuente 6ee0dd9730 chore: prepare Prowler Cloud plugin for the Claude directory (#13098) 2026-10-20 08:39:47 -07:00
Rajesh NadimintiandRubén De la Torre Vico 4980ded0ae feat(gcp): add vertexai workbench terminal, download and auto-upgrade… (#13077)
Co-authored-by: Rubén De la Torre Vico <98956809+puchy22@users.noreply.github.com>
2026-10-20 12:43:06 +02:00
Alejandro Bailo 4d922da2a6 refactor(ui): remove the skill launcher flag and keep the chip rail (#13097) 2026-10-19 20:19:07 +02:00
Alejandro Bailo 76cde39b08 feat(ui): redesign the Exploitability card and the issues verdict filter (#13093) 2026-10-19 17:57:15 +02:00
Pedro Martín 414dd64545 fix(api): scope metadata endpoints to role provider groups (#12964) 2026-10-19 17:15:09 +02:00
Alejandro BailoandClaude Fable 5.1 3654e5a810 fix(ui): drop the New badges from the Alerts and Scans sidebar items (#13092)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 16:32:16 +02:00
Alejandro Bailo 7f9eb2cb11 feat(ui): gate the Issues feature behind the issues-feature flag (#13090) 2026-10-19 16:00:26 +02:00
Alejandro Bailo ba0e78188b fix(ui): keep the E2E suites off the first-run redirect and pick the invite role (#13091) 2026-10-19 15:58:16 +02:00
Alejandro Bailo 1783527c11 refactor(ui): centralize PostHog feature flag reads in ui/lib/feature-flags (#13089) 2026-10-19 12:27:46 +02:00
Alejandro Bailo f46b86101a feat(ui): add issues views using the shared design system (#13068) 2026-10-19 11:34:25 +02:00
Pedro Martín f45173997b fix(api): wrap long account IDs in PDF report cover table (#13063) 2026-10-16 10:33:07 +02:00
Rajesh NadimintiandRubén De la Torre Vico 136e4efdb5 feat(gcp): add vertexai service with workbench public IP and root access checks (#13062)
Co-authored-by: Rubén De la Torre Vico <98956809+puchy22@users.noreply.github.com>
2026-10-16 10:20:34 +02:00
Pedro Martín de4db487d5 fix(container): bump Trivy to 0.76.0 in SDK and API images (#13064) 2026-10-16 09:55:18 +02:00
Prowler Botandprowler-bot f5aaec21cc chore(release): Bump versions to v5.46.0 (#13056)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-10-15 13:28:01 +02:00
Pedro Martín 64ce5a7503 fix(api): pin Redoc to 2.5.1 so the docs render (#13053) 2026-10-15 12:01:03 +02:00
César Arroba Vico 2600d49195 fix(ci): create the release tracking ref locally in prepare-release (#13052) 2026-10-15 11:51:12 +02:00
César Arroba Vico 7c4b12b655 fix(ci): fetch the release branch after creating it in prepare-release (#13050) 2026-10-15 11:23:57 +02:00
506 changed files with 19696 additions and 3304 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@
{
"name": "prowler",
"source": "./claude_plugins/prowler",
"description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.",
"description": "Prowler Cloud security and compliance skills for Claude, powered by the Prowler MCP server. Triage compliance gaps and remediate findings on AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and many more providers listed at https://registry.prowler.com, plus custom providers, until your account meets the framework you choose.",
"category": "security",
"homepage": "https://prowler.com"
}
+1 -1
View File
@@ -174,7 +174,7 @@ SENTRY_RELEASE=local
# REO_DEV_CLIENT_ID=
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.45.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.46.1
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+1
View File
@@ -42,6 +42,7 @@ 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,6 +81,7 @@ 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,6 +85,7 @@ 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
@@ -77,6 +77,7 @@ 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,6 +49,7 @@ 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 -1
View File
@@ -117,7 +117,8 @@ jobs:
fi
echo "Branch $BRANCH_NAME does not exist, creating it from master ($MASTER_SHA)..."
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/heads/${BRANCH_NAME}" -f sha="${MASTER_SHA}"
git checkout -b "$BRANCH_NAME" "$MASTER_SHA"
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."
exit 1
+1
View File
@@ -40,6 +40,7 @@ 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,6 +81,7 @@ 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
@@ -78,6 +78,7 @@ 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
+10 -1
View File
@@ -39,7 +39,10 @@ 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')
runs-on: ubuntu-latest
# 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
env:
AUTH_SECRET: "fallback-ci-secret-for-testing"
AUTH_TRUST_HOST: true
@@ -391,6 +394,12 @@ 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()
+4 -1
View File
@@ -21,7 +21,10 @@ permissions: {}
jobs:
ui-tests:
runs-on: ubuntu-latest
# 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
timeout-minutes: 20
permissions:
contents: read
+10 -122
View File
@@ -7,119 +7,6 @@
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
# Go 1.27.1 and golang.org/x/net v0.59.0 in Trivy 0.75.0, the latest release. Fixed in
# Go 1.27.2 and x/net 0.60.0, which only Trivy main has. Almost every entry is an HTTP,
# HTTP/2 or TLS server path (or Windows-only), and Trivy runs here only as a client. The
# two client-side ones are a DoS from a hostile HTTP/2 peer (CVE-2026-78669) and an
# unframed CONNECT body sent through a proxy (CVE-2026-56866); Trivy's only peer is the
# TLS-authenticated database registry and it never sends CONNECT with a body. Pinned to
# the embedded versions. Remove with the Trivy exception by 2026-11-15.
- vulnerability: CVE-2026-56857
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-56866
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-78659
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-78660
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-78663
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-78669
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-94439
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-94440
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-97031
package:
name: stdlib
version: go1.27.1
- vulnerability: CVE-2026-78659
package:
name: golang.org/x/net
version: v0.59.0
- vulnerability: CVE-2026-78660
package:
name: golang.org/x/net
version: v0.59.0
- vulnerability: CVE-2026-78663
package:
name: golang.org/x/net
version: v0.59.0
- vulnerability: CVE-2026-78669
package:
name: golang.org/x/net
version: v0.59.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
@@ -193,15 +80,16 @@ ignore:
package:
name: python
# zlib in the Alpine base images of the UI (node:24.18.1-alpine) and MCP
# (python:3.13.14-alpine3.23) containers. CVE-2026-85091 is a heap overflow in
# gz_vacate() reached only through non-blocking gzwrite() followed by gzprintf() after
# a write stall. Node links its own bundled zlib and the MCP server never writes gzip
# files, so neither image calls that path. TEMPORARY: the 1.3.2-r1 fix is in the
# Alpine index and could be taken with `apk add --upgrade`, but we wait for the base
# images to ship it instead (the newest node:24-alpine and python:3.13-alpine3.23 still
# carry 1.3.2-r0). Pinned to that version so the rule stops matching on its own once a
# base image moves forward. Remove when the base digests are bumped, by 2026-11-07.
# 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:
+16 -48
View File
@@ -197,40 +197,25 @@ vulnerabilities:
purls:
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
# CVE-2026-78667 and CVE-2026-97031 are DoS bugs in the Go standard library: net/http
# burns CPU parsing a Range header with a large number of small ranges in FileServer,
# ServeContent and ServeFile, and crypto/tls lets a client exhaust server memory with
# multiple ECH outer extension references. Both are server-side code paths. Fixed in
# Go 1.26.9 and 1.27.2 (released 2026-10-08). Trivy 0.75.0, the latest published
# release and the version the images ship, is built with go1.27.1, and Trivy main
# still declares `go 1.27.0`:
# https://github.com/aquasecurity/trivy/blob/v0.75.0/go.mod
# Prowler invokes Trivy exclusively as `trivy fs` and `trivy image` on a local path,
# never `trivy server`, so no HTTP or TLS listener ever exists in the image; its only
# TLS use is as a client pulling the vulnerability database. The purl is pinned to the
# stdlib version so the entry retires itself on the next Trivy bump. Remove this
# temporary suppression as soon as a Trivy release is built with Go >= 1.27.2.
- id: CVE-2026-78667
# 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:golang/stdlib@v1.27.1"
expired_at: 2026-11-15
- id: CVE-2026-97031
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
- id: CVE-2026-111768
purls:
- "pkg:golang/stdlib@v1.27.1"
expired_at: 2026-11-15
# CVE-2026-78669 is an HTTP/2 DoS in the same Trivy 0.75.0 binary (stdlib 1.27.1 and
# golang.org/x/net 0.59.0): a malicious peer burns CPU with many small SETTINGS frames.
# Unlike the two above it also hits clients, but Trivy's only HTTP/2 peer here is the
# TLS-authenticated registry serving the vulnerability database, and the worst case is
# a slow scan. Fixed in Go 1.27.2 and x/net 0.60.0, which Trivy main already pulls in
# but no release ships yet. Same retirement rule as above.
- id: CVE-2026-78669
- "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1"
expired_at: 2027-01-31
- id: CVE-2026-111755
purls:
- "pkg:golang/stdlib@v1.27.1"
- "pkg:golang/golang.org/x/net@v0.59.0"
expired_at: 2026-11-15
- "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
@@ -241,20 +226,3 @@ vulnerabilities:
purls:
- "pkg:nuget/System.Security.Cryptography.Pkcs@10.0.0"
expired_at: 2026-12-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
+10 -7
View File
@@ -8,15 +8,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.75.0
ARG TRIVY_VERSION=0.76.1
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=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
ARG TRIVY_SHA256_AMD64=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
ARG TRIVY_SHA256_ARM64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
@@ -30,6 +30,7 @@ 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.
@@ -40,7 +41,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 \
libsqlite3-0 gzip perl-base libssh2-1t64 libpcre2-8-0 libbrotli1 \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
@@ -121,13 +122,15 @@ 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.12.0
pip install --no-cache-dir uv==0.13.4
RUN uv sync --locked --compile-bytecode && \
rm -rf ~/.cache/uv
# Install PowerShell modules
RUN .venv/bin/python prowler/providers/m365/lib/powershell/m365_powershell.py
# 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
USER root
+19
View File
@@ -4,6 +4,25 @@ All notable changes to the **Prowler API** are documented in this file.
<!-- changelog: release notes start -->
## [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
+8 -6
View File
@@ -7,15 +7,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
# Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com)
ENV POWERSHELL_TELEMETRY_OPTOUT=1
ARG TRIVY_VERSION=0.75.0
ARG TRIVY_VERSION=0.76.1
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=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
ARG TRIVY_SHA256_AMD64=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
ARG TRIVY_SHA256_ARM64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
@@ -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.12.0
pip install --no-cache-dir uv==0.13.4
ENV PATH="/home/prowler/.local/bin:$PATH"
@@ -134,8 +134,10 @@ 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.
RUN .venv/bin/python -m prowler.providers.m365.lib.powershell.m365_powershell
# 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
USER root
+8 -8
View File
@@ -34,7 +34,7 @@ dependencies = [
"django-filter==24.3",
"django-guid==3.5.0",
"django-postgres-extra (==2.0.9)",
"djangorestframework==3.15.2",
"djangorestframework==3.18.3",
"djangorestframework-jsonapi==7.0.2",
"djangorestframework-simplejwt (==5.5.1)",
"drf-nested-routers (==0.95.0)",
@@ -45,7 +45,7 @@ dependencies = [
"gunicorn==26.0.0",
"uvloop==0.22.1",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.45",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.46",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -71,7 +71,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.46.0"
version = "1.47.1"
# 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.3",
"aiohttp==3.14.5",
"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.1.4",
"certifi==2026.10.19",
"cffi==2.0.0",
"charset-normalizer==3.4.4",
"circuitbreaker==2.1.3",
@@ -234,7 +234,7 @@ constraint-dependencies = [
"django-postgres-extra==2.0.9",
"django-silk==5.3.2",
"django-timezone-field==7.2.1",
"djangorestframework==3.15.2",
"djangorestframework==3.18.3",
"djangorestframework-jsonapi==7.0.2",
"djangorestframework-simplejwt==5.5.1",
"dnspython==2.8.0",
@@ -350,7 +350,7 @@ constraint-dependencies = [
"pagerduty==6.1.0",
"pandas==2.2.3",
"pbr==7.0.3",
"pillow==12.3.0",
"pillow==13.0.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.22.0",
"yarl==1.25.1",
"zipp==3.23.0",
"zope-event==6.1",
"zope-interface==8.2",
@@ -12,7 +12,7 @@ Two responsibilities:
Label-injection pipeline:
1. **Protect** string literals and line comments (placeholder replacement).
1. **Protect** string literals and 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,10 +25,12 @@ 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 comments
# Single combined regex: strings first, then line and block 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]*")
_PROTECTED_RE = re.compile(
r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"|//[^\n]*|/\*[\s\S]*?\*/"
)
# Step 2 - Clause splitting
# `OPTIONAL MATCH` must come before `MATCH` to avoid partial matching
@@ -143,18 +145,26 @@ 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(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),
*(
re.compile(rf"\bapoc\s*\.\s*{namespace}\b", re.IGNORECASE)
for namespace in _BLOCKED_APOC_NAMESPACES
),
]
@@ -162,10 +172,11 @@ 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 stripped before matching to avoid
false positives.
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`.
"""
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,6 +45,10 @@ 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,6 +7,7 @@ path while phase-1 drains tenant DBs.
import atexit
import logging
import re
import threading
import time
from collections.abc import Iterator
@@ -186,6 +187,13 @@ 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,6 +15,7 @@ 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
@@ -135,7 +136,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)
return _serialize_graph(graph, provider_id, backend)
except graph_database.WriteQueryNotAllowedException:
raise PermissionDenied(
@@ -174,6 +175,8 @@ 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);
@@ -193,7 +196,7 @@ def execute_custom_query(
try:
graph = backend.execute_read_query(database_name, cypher, None)
serialized = _serialize_graph(graph, provider_id)
serialized = _serialize_graph(graph, provider_id, backend)
return _truncate_graph(serialized)
except graph_database.ClientStatementException as exc:
@@ -265,13 +268,15 @@ def _truncate_graph(graph: dict[str, Any]) -> dict[str, Any]:
return graph
def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
def _serialize_graph(graph, provider_id: str, backend: SinkDatabase) -> 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:
if provider_label not in node.labels or not backend.is_persisted_element_id(
node.element_id
):
continue
kept_node_ids.add(node.element_id)
@@ -286,7 +291,7 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
filtered_count = len(graph.nodes) - len(nodes)
if filtered_count > 0:
logger.debug(
f"Filtered {filtered_count} nodes without provider label {provider_label}"
f"Filtered {filtered_count} nodes without provider label {provider_label} or not stored"
)
relationships = []
@@ -294,6 +299,7 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
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.46.0
version: 1.47.1
description: |-
Prowler API specification.
+66 -1
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)
result = views_helpers._serialize_graph(graph, provider_id, _all_persisted())
assert len(result["nodes"]) == 1
assert result["nodes"][0]["id"] == "n1"
@@ -329,6 +329,71 @@ 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,6 +222,12 @@ 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
@@ -439,6 +445,35 @@ 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",
[
@@ -448,6 +483,8 @@ 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",
@@ -456,6 +493,8 @@ 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,6 +12,7 @@ from api.models import (
ProviderSecret,
Role,
RoleProviderGroupRelationship,
SAMLConfiguration,
Scan,
User,
UserRoleRelationship,
@@ -1896,6 +1897,90 @@ 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.
+181
View File
@@ -98,6 +98,11 @@ 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:
@@ -7089,6 +7094,99 @@ 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):
@@ -7460,6 +7558,42 @@ 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
):
@@ -9092,6 +9226,53 @@ 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
):
+55 -16
View File
@@ -351,6 +351,15 @@ 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,
@@ -799,7 +808,9 @@ class SAMLConfigurationViewSet(BaseRLSViewSet):
"""
serializer_class = SAMLConfigurationSerializer
required_permissions = [Permissions.MANAGE_INTEGRATIONS]
# 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]
queryset = SAMLConfiguration.objects.all()
def get_queryset(self):
@@ -1896,7 +1907,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
self.required_permissions = [Permissions.MANAGE_PROVIDERS]
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = self.user_role
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all providers
queryset = Provider.objects.filter(tenant_id=self.request.tenant_id)
@@ -3058,7 +3069,7 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = self.user_role
base_queryset = AttackPathsScan.objects.filter(tenant_id=self.request.tenant_id)
if user_roles.unlimited_visibility:
@@ -3412,7 +3423,7 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
required_permissions = []
def get_queryset(self):
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = self.user_role
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all scans
queryset = Resource.all_objects.filter(tenant_id=self.request.tenant_id)
@@ -3544,8 +3555,15 @@ 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]"
@@ -3623,7 +3641,10 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
).values_list("groups", flat=True)
)
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)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -3648,9 +3669,10 @@ 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 = Scan.all_objects.filter(
tenant_id=tenant_id
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
).latest_per_provider()
queryset = ResourceScanSummary.objects.filter(
@@ -3691,7 +3713,10 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
all_groups = Resource.objects.filter(
tenant_id=tenant_id,
groups__isnull=False,
).values_list("groups", flat=True)
)
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)
groups = sorted(
{g for groups_list in all_groups if groups_list for g in groups_list}
)
@@ -4028,7 +4053,7 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
def get_queryset(self):
tenant_id = self.request.tenant_id
user_roles = get_role(self.request.user, self.request.tenant_id)
user_roles = self.user_role
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all findings
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
@@ -4102,8 +4127,16 @@ 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
@@ -4161,7 +4194,10 @@ class FindingViewSet(PaginateByPkMixin, BaseRLSViewSet):
if not queryset.exists():
raw_scans_ids = Scan.objects.filter(
tenant_id=tenant_id, **scan_based_filters
).values_list("id", "unique_resource_count")
)
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")
scan_ids = [
scan_id for scan_id, count in raw_scans_ids if count and count > 0
]
@@ -4203,11 +4239,13 @@ 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(
ScanCategorySummary.objects.filter(
tenant_id=tenant_id, **category_scan_filters
)
.values_list("category", flat=True)
category_summaries.values_list("category", flat=True)
.distinct()
.order_by("category")
)
@@ -4262,9 +4300,10 @@ 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 = Scan.all_objects.filter(
tenant_id=tenant_id
latest_scans_queryset = _scans_visible_to(
user_roles, tenant_id
).latest_per_provider()
raw_latest_scans_ids = list(
latest_scans_queryset.values_list("id", "unique_resource_count")
+41
View File
@@ -443,6 +443,47 @@ 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(
+4 -8
View File
@@ -172,14 +172,10 @@ class CISReportGenerator(BaseComplianceReportGenerator):
# Metadata table via base class helper
info_rows = self._build_info_rows(data, language=self.config.language)
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_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
metadata_table = Table(metadata_data, colWidths=[2 * inch, 4 * inch])
metadata_table.setStyle(
+5 -10
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 get_color_for_compliance
from .components import escape_html, get_color_for_compliance
from .config import (
COLOR_BG_BLUE,
COLOR_BLUE,
@@ -99,15 +99,10 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
# Compliance info table - use base class helper for consistency
info_rows = self._build_info_rows(data, language="es")
# 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_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
info_table = Table(info_data, colWidths=[2 * inch, 4 * inch])
info_table.setStyle(
+5 -9
View File
@@ -11,6 +11,7 @@ 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,
@@ -111,15 +112,10 @@ class NIS2ReportGenerator(BaseComplianceReportGenerator):
# Compliance metadata table - use base class helper for consistency
info_rows = self._build_info_rows(data, language="en")
# 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_data = [
[label, Paragraph(escape_html(str(value)), self.styles["normal_center"])]
for label, value in info_rows
]
metadata_table = Table(metadata_data, colWidths=[2 * inch, 4 * inch])
metadata_table.setStyle(
+5 -4
View File
@@ -997,11 +997,12 @@ def _process_finding_micro_batch(
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.type = finding.resource_type
updated = True
if resource_instance.metadata != finding.resource_metadata:
serialized_metadata = json.dumps(
finding.resource_metadata, cls=CustomEncoder
)
if resource_instance.metadata != serialized_metadata:
snapshot_resource_fields(resource_uid, resource_instance)
resource_instance.metadata = json.dumps(
finding.resource_metadata, cls=CustomEncoder
)
resource_instance.metadata = serialized_metadata
updated = True
if resource_instance.details != finding.resource_details:
snapshot_resource_fields(resource_uid, resource_instance)
+7 -1
View File
@@ -897,8 +897,12 @@ 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) for f in batch
FindingOutput.transform_api_finding(
f, prowler_provider, metadata_cache=metadata_cache
)
for f in batch
]
# Outputs
@@ -979,6 +983,8 @@ 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(
@@ -2,6 +2,7 @@ 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
@@ -582,3 +583,24 @@ 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,6 +2,7 @@ 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
@@ -189,6 +190,31 @@ 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,6 +2,7 @@ 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
@@ -214,6 +215,31 @@ 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,6 +2349,78 @@ 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: f,
side_effect=lambda f, _prov, **_kw: f,
),
patch("tasks.tasks._compress_output_files", return_value="outdir.zip"),
patch("tasks.tasks._upload_to_s3", return_value="s3://bucket/outdir.zip"),
+17 -1
View File
@@ -1,10 +1,11 @@
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 batched, get_next_execution_datetime
from tasks.utils import CustomEncoder, batched, get_next_execution_datetime
@pytest.mark.django_db
@@ -100,3 +101,18 @@ 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,6 +19,11 @@ 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
+122 -114
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.3" },
{ name = "aiohttp", specifier = "==3.14.5" },
{ 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.1.4" },
{ name = "certifi", specifier = "==2026.10.19" },
{ name = "cffi", specifier = "==2.0.0" },
{ name = "charset-normalizer", specifier = "==3.4.4" },
{ name = "circuitbreaker", specifier = "==2.1.3" },
@@ -150,7 +150,7 @@ 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.15.2" },
{ name = "djangorestframework", specifier = "==3.18.3" },
{ name = "djangorestframework-jsonapi", specifier = "==7.0.2" },
{ name = "djangorestframework-simplejwt", specifier = "==5.5.1" },
{ name = "dnspython", specifier = "==2.8.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 = "==12.3.0" },
{ name = "pillow", specifier = "==13.0.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.22.0" },
{ name = "yarl", specifier = "==1.25.1" },
{ name = "zipp", specifier = "==3.23.0" },
{ name = "zope-event", specifier = "==6.1" },
{ name = "zope-interface", specifier = "==8.2" },
@@ -480,7 +480,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.14.3"
version = "3.14.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -492,44 +492,45 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "yarl" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -1862,11 +1863,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.1.4"
version = "2026.10.19"
source = { registry = "https://pypi.org/simple" }
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -2523,14 +2524,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.15.2"
version = "3.18.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -4661,33 +4662,38 @@ wheels = [
[[package]]
name = "pillow"
version = "12.3.0"
version = "13.0.0"
source = { registry = "https://pypi.org/simple" }
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -4861,8 +4867,8 @@ wheels = [
[[package]]
name = "prowler"
version = "5.45.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.45#9b56a3b657ef72c456a50ae9c9a0b044ce81dd5c" }
version = "5.46.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.46#fdb1bd619345b6f92bd6a7f8906fa5e6684a90a4" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4966,7 +4972,7 @@ dependencies = [
[[package]]
name = "prowler-api"
version = "1.46.0"
version = "1.47.1"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -5050,7 +5056,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.15.2" },
{ name = "djangorestframework", specifier = "==3.18.3" },
{ name = "djangorestframework-jsonapi", specifier = "==7.0.2" },
{ name = "djangorestframework-simplejwt", specifier = "==5.5.1" },
{ name = "drf-nested-routers", specifier = "==0.95.0" },
@@ -5066,7 +5072,7 @@ requires-dist = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.45" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.46" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
@@ -6594,48 +6600,50 @@ wheels = [
[[package]]
name = "yarl"
version = "1.22.0"
version = "1.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -1,8 +1,9 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "prowler",
"displayName": "Prowler Cloud",
"version": "0.1.0",
"description": "Prowler for Claude Code — cloud security and compliance skills powered by the Prowler MCP server. Bundles compliance triage and remediation; more skills coming.",
"description": "Prowler Cloud security and compliance skills for Claude, powered by the Prowler MCP server. Triage compliance gaps and remediate findings on AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub and many more providers listed at https://registry.prowler.com, plus custom providers, until your account meets the framework you choose.",
"author": {
"name": "Prowler",
"email": "support@prowler.com",
@@ -12,11 +13,26 @@
"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",
"compliance",
"cloud-security",
"mcp"
"vercel"
],
"userConfig": {
"api_key": {
+7 -5
View File
@@ -1,9 +1,11 @@
{
"prowler": {
"type": "http",
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer ${user_config.api_key}"
"mcpServers": {
"prowler": {
"type": "http",
"url": "https://mcp.prowler.com/mcp",
"headers": {
"Authorization": "Bearer ${user_config.api_key}"
}
}
}
}
+201
View File
@@ -0,0 +1,201 @@
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.
+15 -1
View File
@@ -4,6 +4,14 @@ End-to-end cloud security and compliance from inside [Claude Code](https://www.c
> **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.
@@ -12,6 +20,8 @@ End-to-end cloud security and compliance from inside [Claude Code](https://www.c
## 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
@@ -58,6 +68,10 @@ 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, and can trigger scans. 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
@@ -77,4 +91,4 @@ The stored API key is removed automatically.
## License
Apache 2.0 — see [LICENSE](../../LICENSE).
Apache 2.0 — see [LICENSE](LICENSE).
+24 -1
View File
@@ -164,6 +164,28 @@ 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:
@@ -501,8 +523,9 @@ 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.2"
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
python_latest_version: "3.12"
# azure.app_ensure_java_version_is_latest
+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) -> "Finding":
def transform_api_finding(cls, finding, provider, metadata_cache=None) -> "Finding":
# ... existing code ...
# Your provider specific field
@@ -128,8 +128,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.44.0"
PROWLER_API_VERSION="5.44.0"
PROWLER_UI_VERSION="5.45.0"
PROWLER_API_VERSION="5.45.0"
```
<Note>
@@ -39,6 +39,9 @@ The following list includes all the AWS checks with configurable variables that
| `awslambda_function_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | List of Strings | See `config.yaml` |
| `awslambda_function_vpc_multi_az` | `lambda_min_azs` | Integer | `2` |
| `elasticbeanstalk_environment_php_platform_updated` | `elasticbeanstalk_php_min_branch` | String | `"8.2"` |
| `elasticbeanstalk_environment_php_platform_updated` | `elasticbeanstalk_php_min_platform_version` | String | `"4.13.13"` |
| `lightsail_instance_php_blueprint_updated` | `lightsail_php_blueprints` | List of Strings | See `config.yaml` |
| `cloudformation_stack_cdktoolkit_bootstrap_version` | `recommended_cdk_bootstrap_version` | Integer | `21` |
| `cloudformation_stack_outputs_find_secrets` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `cloudfront_distributions_pqc_tls_enabled` | `cloudfront_pqc_min_protocol_versions` | List of Strings | `["TLSv1.3_2025"]` |
@@ -214,7 +217,7 @@ The following list includes all the Azure checks with configurable variables tha
| `apim_threat_detection_llm_jacking` | `apim_threat_detection_llm_jacking_minutes` | Integer | `1440` |
| `apim_threat_detection_llm_jacking` | `apim_threat_detection_llm_jacking_threshold` | Float | `0.1` |
| `app_ensure_java_version_is_latest` | `java_latest_version` | String | `"17"` |
| `app_ensure_php_version_is_latest` | `php_latest_version` | String | `"8.2"` |
| `app_ensure_php_version_is_latest` | `php_latest_version` | String | `"8.5"` |
| `app_ensure_python_version_is_latest` | `python_latest_version` | String | `"3.12"` |
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String | `"High"` |
| `network_public_ip_shodan` | `shodan_api_key` | String | `null` |
@@ -231,6 +234,7 @@ The following list includes all the GCP checks with configurable variables that
| Check Name | Value | Type | Default |
|---------------------------------------------------|-----------------------------------|---------|---------|
| `appengine_version_php_runtime_supported` | `appengine_php_min_version` | String | `"8.2"` |
| `cloudstorage_bucket_sufficient_retention_period` | `storage_min_retention_days` | Integer | `90` |
| `compute_instance_group_multiple_zones` | `mig_min_zones` | Integer | `2` |
| `compute_public_address_shodan` | `shodan_api_key` | String | `null` |
@@ -448,6 +452,28 @@ aws:
# 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:
@@ -718,8 +744,9 @@ azure:
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.2"
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
python_latest_version: "3.12"
# azure.app_ensure_java_version_is_latest
@@ -25,6 +25,14 @@ Prowler requests the following read-only OAuth 2.0 scopes:
The delegated user must be a **super administrator** in your Google Workspace organization. Using a non-admin account will result in permission errors when accessing the Admin SDK.
</Warning>
### Optional Scope for Edition Context
Prowler can read which Google Workspace editions the tenant holds, Cloud Identity Premium included, and state in a finding when the audited feature is not sold on them (for example, Data Loss Prevention (DLP) for Drive or Google session control). This needs the Enterprise License Manager API enabled in the GCP project (Step 2) and the scope `https://www.googleapis.com/auth/apps.licensing` added to the Domain-Wide Delegation list (Step 5). Without the scope, every check runs the same and findings carry no edition context.
<Warning>
The `apps.licensing` scope is Google's "view and manage licenses" scope and has no read-only variant. Prowler only lists license assignments, but anyone holding the Service Account key could assign or revoke licenses with it on behalf of the super administrator. Grant it only if you want the edition context, and protect the key accordingly.
</Warning>
## Setup Steps
### Step 1: Create a Google Cloud Platform (GCP) Project (if Needed)
@@ -35,21 +43,22 @@ The project is only used to host the Service Account — it does not need to hav
### Step 2: Enable Required APIs
In the [Google Cloud Console](https://console.cloud.google.com), select the target project and navigate to **APIs & Services → Library**. Search for and enable each of the following APIs:
In the [Google Cloud Console](https://console.cloud.google.com), select the target project and enable each of the following APIs:
| API | Required For |
|-----|--------------|
| **Admin SDK API** | Directory service checks (users, roles, domains) |
| **Cloud Identity API** | All service checks except Directory (domain-level application policies) |
| API | Service Name | Required For |
|-----|--------------|--------------|
| **[Admin SDK API](https://console.cloud.google.com/apis/library/admin.googleapis.com)** | `admin.googleapis.com` | Directory service checks (users, roles, domains) |
| **[Cloud Identity API](https://console.cloud.google.com/apis/library/cloudidentity.googleapis.com)** | `cloudidentity.googleapis.com` | All service checks except Directory (domain-level application policies) |
| **[Enterprise License Manager API](https://console.cloud.google.com/apis/library/licensing.googleapis.com)** | `licensing.googleapis.com` | Optional: edition context in findings (see Optional Scope for Edition Context) |
For each API:
1. Search for the API name in the library
2. Click the API result
1. Open the link above, or navigate to **APIs & Services → Library** and search for the API name
2. Confirm the target project is selected in the project picker
3. Click **Enable**
<Note>
Both APIs must be enabled in the same GCP project that hosts the Service Account. All service checks except Directory will return no findings if the Cloud Identity API is not enabled.
The Admin SDK API and the Cloud Identity API must be enabled in the same GCP project that hosts the Service Account. All service checks except Directory will return no findings if the Cloud Identity API is not enabled. The Identity and Access Management (IAM) API (`iam.googleapis.com`) is a different API: enabling it does not replace the Cloud Identity API.
</Note>
### Step 3: Create a Service Account
@@ -77,6 +86,30 @@ The Service Account does not need any GCP IAM roles. Its access to Google Worksp
This JSON key grants access to your Google Workspace organization. Never commit it to version control, share it in plain text, or store it in an insecure location.
</Warning>
#### Service Account Key Creation Is Disabled
Organizations created on or after May 3, 2024 enforce the [Secure by Default](https://docs.cloud.google.com/resource-manager/docs/secure-by-default-organizations) organization policy `iam.managed.disableServiceAccountKeyCreation`, and the **Add key** dialog shows "Service account key creation is disabled". To create the key, a user with the Organization Policy Administrator role (`roles/orgpolicy.policyAdmin`) on the organization must turn off enforcement for the project that hosts the Service Account:
1. In the Google Cloud Console, select the project and navigate to **IAM & Admin → Organization Policies**
2. Open **Disable service account key creation** (`iam.managed.disableServiceAccountKeyCreation`) and click **Manage policy**
3. Select **Override parent's policy**, click **Add a rule**, set **Enforcement** to **Off**, and click **Set policy**
4. Return to the Service Account **Keys** tab and create the key
Alternatively, override the policy with the Google Cloud CLI:
```yaml policy.yaml
name: projects/PROJECT_ID/policies/iam.managed.disableServiceAccountKeyCreation
spec:
rules:
- enforce: false
```
```console
gcloud org-policies set-policy policy.yaml
```
The constraint only blocks new key creation. Keys that already exist keep working, so enforcement can be restored once the key has been downloaded.
### Step 5: Configure Domain-Wide Delegation in Google Workspace
1. Navigate to the [Google Workspace Admin Console](https://admin.google.com)
@@ -90,6 +123,8 @@ This JSON key grants access to your Google Workspace organization. Never commit
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/admin.directory.orgunit.readonly,https://www.googleapis.com/auth/cloud-identity.policies.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
```
To enable the optional edition context, append `,https://www.googleapis.com/auth/apps.licensing` to the list.
7. Click **Authorize**
<Note>
@@ -611,6 +611,40 @@ If specific checks fail due to insufficient permissions:
Using Public Cloud credentials can limit Keystone API access, so the command above may not work. Verify permissions in the provider's control panel instead.
</Warning>
### Private Keystone Endpoints
Most OpenStack deployments keep Keystone on an internal network, which is the normal case rather than the exception.
**Prowler CLI scans a private cloud with no extra configuration:**
```console
export OS_AUTH_URL="https://keystone.internal:5000/v3"
prowler openstack
```
On the CLI the operator supplies the `auth_url` themselves, so there is nothing to defend against and the check below does not apply.
**Prowler App rejects an `auth_url` that resolves to a non-public address**, as an SSRF defense, before the SDK connects and on both the connection test and the scan. There the URL arrives from a tenant and the worker must not be used to reach the internal network it runs in. To scan a private cloud from Prowler App, declare the trusted ranges:
```console
PROWLER_ALLOWED_PRIVATE_NETWORKS="10.20.0.0/16,192.168.65.254/32"
```
The value is a comma-separated list of IPs and CIDRs. A resolved address inside an allowlisted range is permitted; every other non-public address stays blocked, so link-local (`169.254.169.254`), loopback and the rest of the internal network remain protected. Malformed entries are rejected, and a non-empty allowlist is logged as a relaxed security control.
The scan runs in the worker, not the API, so the variable has to reach the worker:
- **Docker Compose**: set it in the root `.env`; it reaches the worker through the shared environment file.
- **Helm**: add it under `api.djangoConfig`, which is rendered into the API ConfigMap that the worker inherits through `envFrom`.
```yaml
api:
djangoConfig:
PROWLER_ALLOWED_PRIVATE_NETWORKS: "10.20.0.0/16"
```
Setting it only on the API container has no effect. The same variable applies to the Kubernetes and IaC providers. The Image provider has its own, `PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS`.
## Next Steps
- [Getting Started with OpenStack](/user-guide/providers/openstack/getting-started-openstack) - Run your first scan
@@ -249,7 +249,7 @@ Assign administrative permissions by selecting from the following options:
| Permission | Scope | Description |
|------------|-------|-------------|
| Invite and Manage Users | All | Invite new users and manage existing ones. |
| Manage Account | All | Adjust account settings, delete users and read/manage users permissions. |
| Manage Account | All | Adjust account settings, delete users, read/manage users permissions and configure SAML SSO. |
| Manage Scans | All | Run and review scans, and manage [Scan Configuration](/user-guide/tutorials/prowler-app-scan-configuration) settings. |
| Manage Providers | All | Add or modify connected providers, and attach or detach providers from a [Scan Configuration](/user-guide/tutorials/prowler-app-scan-configuration) (in addition to Manage Scans). |
| Manage Integrations | All | Add or modify the Prowler Integrations. |
@@ -41,7 +41,7 @@ If the SAML configuration is removed, users who previously authenticated via SAM
</Warning>
### Prerequisites
- Administrator access to the Prowler organization is required.
- A role with the **Manage Account** permission in the Prowler organization is required.
- Administrative access to the SAML 2.0 compliant Identity Provider (e.g., Okta, Azure AD, Google Workspace) is necessary.
### Configuration Steps
+8
View File
@@ -4,6 +4,14 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
<!-- changelog: release notes start -->
## [0.12.4] (Prowler v5.46.0)
### 🔐 Security
- certifi from 2026.4.22 to 2026.10.19 in the MCP Server container image, dropping the emSign root CAs whose intermediate keys were compromised [(#13107)](https://github.com/prowler-cloud/prowler/pull/13107)
---
## [0.12.3] (Prowler v5.45.0)
### 🔐 Security
+4 -4
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
@@ -129,11 +129,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.4.22"
version = "2026.10.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
{ 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" },
]
[[package]]
+40
View File
@@ -4,6 +4,46 @@ All notable changes to the **Prowler SDK** are documented in this file.
<!-- changelog: release notes start -->
## [5.46.0] (Prowler v5.46.0)
### 🚀 Added
- `cognito_user_pool_account_recovery_sms_not_preferred` check verifying Cognito user pools do not rank SMS first for account recovery [(#12251)](https://github.com/prowler-cloud/prowler/pull/12251)
- `bedrock_guardrail_sensitive_information_filter_enforced` check for AWS provider, flagging Bedrock 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 [(#12663)](https://github.com/prowler-cloud/prowler/pull/12663)
- `bedrock_model_invocation_logging_text_data_enabled` check for AWS provider, verifying that model invocation logging delivers prompt and response text rather than naming a destination while excluding request and response bodies [(#12663)](https://github.com/prowler-cloud/prowler/pull/12663)
- Vertex AI service for GCP provider, including the `vertexai_workbench_instance_public_ip_disabled` and `vertexai_workbench_instance_root_access_disabled` checks mapped to PCI DSS 4.0 [(#13062)](https://github.com/prowler-cloud/prowler/pull/13062)
- `vertexai_workbench_instance_terminal_access_disabled`, `vertexai_workbench_instance_file_download_disabled` and `vertexai_workbench_instance_auto_upgrade_enabled` checks for GCP provider, with Vertex AI Workbench checks mapped to CCC MLDE requirements [(#13077)](https://github.com/prowler-cloud/prowler/pull/13077)
- Google Workspace findings state when a feature is not sold on the tenant edition: Drive DLP rules, Google session control and the shared drive sharing settings on Business Starter; reading editions needs the optional `apps.licensing` scope [(#13113)](https://github.com/prowler-cloud/prowler/pull/13113)
- `wafv2_webacl_managed_rule_groups_enabled` check to verify WAFv2 Web ACLs reference at least one managed rule group [(#13127)](https://github.com/prowler-cloud/prowler/pull/13127)
- `appengine` GCP service and `appengine_version_php_runtime_supported` check to flag App Engine serving versions on PHP runtimes without security fixes [(#13127)](https://github.com/prowler-cloud/prowler/pull/13127)
- `elasticbeanstalk_environment_php_platform_updated` check to flag Elastic Beanstalk PHP environments on branches without security fixes or on platform versions older than the one bundling the fixed PHP release [(#13127)](https://github.com/prowler-cloud/prowler/pull/13127)
- `lightsail_instance_php_blueprint_updated` check to flag Lightsail instances created from PHP blueprints for manual verification of the PHP release [(#13127)](https://github.com/prowler-cloud/prowler/pull/13127)
### 🔄 Changed
- Google Workspace 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. The rule covers settings that only act while the app is on: Drive sharing settings and the settings of existing groups keep being evaluated because existing shares and groups stay reachable, and only group creation is exempt when Groups for Business is off [(#13117)](https://github.com/prowler-cloud/prowler/pull/13117)
- Default `php_latest_version` for `app_ensure_php_version_is_latest` raised from `8.2` to `8.5`, the newest PHP runtime on Azure App Service for Linux, following the PHP2Shell (CVE-2026-109618, CVE-2026-109619) security release [(#13128)](https://github.com/prowler-cloud/prowler/pull/13128)
### 🐞 Fixed
- `bedrock_api_key_no_long_term_credentials` now reports a Bedrock long-term API key that never expires; IAM only returns `ExpirationDate` for keys created with an expiration period, so keys created without one were skipped and produced no finding at all [(#12663)](https://github.com/prowler-cloud/prowler/pull/12663)
- `drive_desktop_access_disabled` remediation steps for Google Workspace now point to the Google Drive for desktop card in the Admin console instead of Features and Applications [(#13113)](https://github.com/prowler-cloud/prowler/pull/13113)
- 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 [(#13114)](https://github.com/prowler-cloud/prowler/pull/13114)
- Google Workspace `drive_publishing_files_disabled` passes when sharing outside the organization is limited to allowlisted domains or off, since publishing to the web only exists with sharing on [(#13115)](https://github.com/prowler-cloud/prowler/pull/13115)
- Google Workspace checks now read the policy Google applies (greatest sortOrder) instead of the last policy the Cloud Identity Policy API lists, which reported default values over admin-configured ones [(#13117)](https://github.com/prowler-cloud/prowler/pull/13117)
- Azure App Service runtime checks (`app_ensure_php_version_is_latest`, `app_ensure_python_version_is_latest`, `app_ensure_java_version_is_latest`) now pass any runtime at or above the configured minimum instead of failing every version newer than the configured one [(#13127)](https://github.com/prowler-cloud/prowler/pull/13127)
- Scan report generation parses each resource's metadata once per batch and sanitizes it once per finding, so providers with very large resource metadata no longer exhaust the worker memory [(#13149)](https://github.com/prowler-cloud/prowler/pull/13149)
### 🔐 Security
- OpenStack auth_url resolving to loopback, private or otherwise non-public hosts rejected before the SDK connects, on both the connection test and the scan [(#12962)](https://github.com/prowler-cloud/prowler/pull/12962)
- Trivy from 0.75.0 to 0.76.0 in the SDK 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)
- certifi to 2026.10.19 and aiohttp to 3.14.5 with yarl 1.25.1 in the SDK, plus uv 0.13.4 and Trivy 0.76.1 in its container image, patching 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)
- 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 [(#13129)](https://github.com/prowler-cloud/prowler/pull/13129)
- black from 26.3.1 to 26.10.1 and graphql-core from 3.2.8 to 3.2.12 in the SDK, and libbrotli1 1.1.0-2+deb13u1 in its container image, which no longer ships the MicrosoftTeams build SBOM that Trivy read as installed zod and body-parser [(#13135)](https://github.com/prowler-cloud/prowler/pull/13135)
---
## [5.45.0] (Prowler v5.45.0)
### 🚀 Added
@@ -123,7 +123,8 @@
"inspector2_active_findings_exist",
"awslambda_function_using_supported_runtimes",
"rds_instance_deprecated_engine_version",
"rds_instance_extended_support"
"rds_instance_extended_support",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -160,7 +161,8 @@
"redshift_cluster_automatic_upgrades",
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"ecs_service_fargate_latest_platform_version"
"ecs_service_fargate_latest_platform_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -215,7 +217,8 @@
"eks_cluster_uses_a_supported_version",
"ecs_service_fargate_latest_platform_version",
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version"
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -60,7 +60,8 @@
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"apigateway_restapi_waf_acl_attached",
"cognito_user_pool_waf_acl_attached"
"cognito_user_pool_waf_acl_attached",
"wafv2_webacl_managed_rule_groups_enabled"
]
},
{
@@ -552,7 +553,8 @@
}
],
"Checks": [
"bedrock_guardrail_sensitive_information_filter_enabled"
"bedrock_guardrail_sensitive_information_filter_enabled",
"bedrock_guardrail_sensitive_information_filter_enforced"
]
},
{
@@ -585,6 +587,7 @@
],
"Checks": [
"bedrock_model_invocation_logging_enabled",
"bedrock_model_invocation_logging_text_data_enabled",
"bedrock_model_invocation_logs_encryption_enabled"
]
},
+2 -1
View File
@@ -5142,7 +5142,8 @@
"ssm_managed_compliant_patching",
"elasticache_redis_cluster_auto_minor_version_upgrades",
"codeartifact_packages_external_public_publishing_disabled",
"elasticbeanstalk_environment_managed_updates_enabled"
"elasticbeanstalk_environment_managed_updates_enabled",
"elasticbeanstalk_environment_php_platform_updated"
]
},
{
@@ -2856,7 +2856,8 @@
"waf_regional_webacl_with_rules",
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"ConfigRequirements": [
{
@@ -3619,7 +3620,8 @@
"rds_instance_deprecated_engine_version",
"rds_instance_minor_version_upgrade_enabled",
"redshift_cluster_automatic_upgrades",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -2859,7 +2859,8 @@
"waf_regional_webacl_with_rules",
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"ConfigRequirements": [
{
@@ -3622,7 +3623,8 @@
"rds_instance_deprecated_engine_version",
"rds_instance_minor_version_upgrade_enabled",
"redshift_cluster_automatic_upgrades",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
+4 -2
View File
@@ -1128,7 +1128,8 @@
"Description": "requirements regarding security updates throughout the entire lifetime of the ICT services or ICT products, or replacement after the end of the support period;",
"Checks": [
"elasticbeanstalk_environment_managed_updates_enabled",
"opensearch_service_domains_updated_to_the_latest_service_software_version"
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -1253,7 +1254,8 @@
"memorydb_cluster_auto_minor_version_upgrades",
"elasticbeanstalk_environment_managed_updates_enabled",
"elasticache_redis_cluster_auto_minor_version_upgrades",
"glue_development_endpoints_job_bookmark_encryption_enabled"
"glue_development_endpoints_job_bookmark_encryption_enabled",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
+7 -2
View File
@@ -280,7 +280,11 @@
"ec2_instance_with_outdated_ami",
"eks_cluster_uses_a_supported_version",
"kafka_cluster_uses_latest_version",
"rds_instance_deprecated_engine_version"
"rds_instance_deprecated_engine_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"gcp": [
"appengine_version_php_runtime_supported"
]
}
},
@@ -3717,7 +3721,8 @@
"waf_regional_rule_with_conditions",
"waf_regional_rulegroup_not_empty",
"waf_regional_webacl_with_rules",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"cloudflare": [
"dns_record_proxied",
+7 -2
View File
@@ -6363,7 +6363,8 @@
"cloudfront_distributions_using_waf",
"guardduty_is_enabled",
"shield_advanced_protection_in_cloudfront_distributions",
"shield_advanced_protection_in_internet_facing_load_balancers"
"shield_advanced_protection_in_internet_facing_load_balancers",
"wafv2_webacl_managed_rule_groups_enabled"
],
"azure": [
"defender_ensure_defender_for_arm_is_on",
@@ -8481,7 +8482,8 @@
"memorydb_cluster_auto_minor_version_upgrades",
"mq_broker_auto_minor_version_upgrades",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"kafka_cluster_uses_latest_version"
"kafka_cluster_uses_latest_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [
"app_ensure_java_version_is_latest",
@@ -8492,6 +8494,9 @@
],
"alibabacloud": [
"ecs_instance_latest_os_patches_applied"
],
"gcp": [
"appengine_version_php_runtime_supported"
]
}
},
+9 -4
View File
@@ -354,7 +354,8 @@
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"rds_instance_deprecated_engine_version",
"wellarchitected_workload_no_high_or_medium_risks"
"wellarchitected_workload_no_high_or_medium_risks",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [],
"gcp": [],
@@ -718,7 +719,8 @@
"waf_regional_rule_with_conditions",
"waf_regional_rulegroup_not_empty",
"waf_regional_webacl_with_rules",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"azure": [
"network_vnet_ddos_protection_enabled",
@@ -1756,7 +1758,8 @@
"ecr_registry_enhanced_scanning_enabled",
"inspector2_active_findings_exist",
"inspector2_is_enabled",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [
"app_ensure_java_version_is_latest",
@@ -1765,7 +1768,9 @@
"app_function_latest_runtime_version",
"defender_ensure_system_updates_are_applied"
],
"gcp": [],
"gcp": [
"appengine_version_php_runtime_supported"
],
"kubernetes": [],
"m365": []
},
+3 -1
View File
@@ -4427,7 +4427,9 @@
"ComplementaryCriteria": ""
}
],
"Checks": []
"Checks": [
"appengine_version_php_runtime_supported"
]
},
{
"Id": "OPS-33.01AS",
+24 -8
View File
@@ -6815,7 +6815,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_root_access_disabled"
]
},
{
"Id": "CCC.MLDE.CN03.AR02",
@@ -6871,7 +6873,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_root_access_disabled"
]
},
{
"Id": "CCC.MLDE.CN04.AR01",
@@ -6923,7 +6927,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_terminal_access_disabled"
]
},
{
"Id": "CCC.MLDE.CN04.AR02",
@@ -6978,7 +6984,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_terminal_access_disabled"
]
},
{
"Id": "CCC.MLDE.CN02.AR01",
@@ -7033,7 +7041,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_file_download_disabled"
]
},
{
"Id": "CCC.MLDE.CN02.AR02",
@@ -7250,7 +7260,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_auto_upgrade_enabled"
]
},
{
"Id": "CCC.MLDE.CN06.AR02",
@@ -7360,7 +7372,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
]
},
{
"Id": "CCC.MLDE.CN07.AR02",
@@ -7416,7 +7430,9 @@
]
}
],
"Checks": []
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
]
},
{
"Id": "CCC.MLDE.CN08.AR01",
+4 -2
View File
@@ -656,7 +656,8 @@
"apikeys_api_restrictions_configured",
"apikeys_key_rotated_in_90_days",
"artifacts_container_analysis_enabled",
"bigquery_dataset_cmk_encryption"
"bigquery_dataset_cmk_encryption",
"appengine_version_php_runtime_supported"
],
"Attributes": [
{
@@ -733,7 +734,8 @@
"Checks": [
"compute_instance_shielded_vm_enabled",
"gcr_container_scanning_enabled",
"artifacts_container_analysis_enabled"
"artifacts_container_analysis_enabled",
"appengine_version_php_runtime_supported"
],
"Attributes": [
{
+39 -13
View File
@@ -701,7 +701,9 @@
"Id": "1.2.8.37",
"Description": "Checks if direct internet access is disabled for a Google AI Platform (formerly AI Platform Notebooks) instance. The rule is NON_COMPLIANT if an AI Platform notebook instance is internet-enabled.",
"Name": "Google AI Platform",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "1.2.8: Network security controls (NSCs) are configured and maintained.",
@@ -1358,7 +1360,9 @@
"Id": "1.3.1.41",
"Description": "Checks if direct internet access is disabled for a Google AI Platform Notebook instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google AI Platform",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "1.3.1: Network access to and from the cardholder data environment is restricted.",
@@ -2029,7 +2033,9 @@
"Id": "1.3.2.41",
"Description": "Checks if direct internet access is disabled for a Google AI Platform (formerly known as AI Platform Notebooks) instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google AI Platform",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "1.3.2: Network access to and from the cardholder data environment is restricted.",
@@ -2805,7 +2811,9 @@
"Id": "1.4.2.39",
"Description": "Checks if direct internet access is disabled for a Google Cloud AI Platform Notebooks instance. The rule is NON_COMPLIANT if an AI Platform Notebooks instance is internet-enabled.",
"Name": "Google Cloud AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "1.4.2: Network connections between trusted and untrusted networks are controlled.",
@@ -3616,7 +3624,9 @@
"Id": "1.5.1.37",
"Description": "Checks if direct internet access is disabled for a Google Cloud Vertex AI Workbench instance. The rule is NON_COMPLIANT if the Vertex AI Workbench instance is internet-enabled.",
"Name": "Google Cloud Vertex AI Workbench",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "1.5.1: Risks to the CDE from computing devices that are able to connect to both untrusted networks and the CDE are mitigated.",
@@ -8491,7 +8501,9 @@
"Id": "10.3.2.25",
"Description": "Checks if direct internet access is disabled for a Google AI Platform Notebook instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "10.3.2: Audit logs are protected from destruction and unauthorized modifications.",
@@ -12122,7 +12134,9 @@
"Id": "3.5.1.3.30",
"Description": "Checks if direct internet access is disabled for a Google Cloud AI Platform Notebooks instance. The rule is NON_COMPLIANT if an AI Platform Notebooks instance is internet-enabled.",
"Name": "Google Cloud AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "3.5.1.3: Primary account number (PAN) is secured wherever it is stored.",
@@ -15753,7 +15767,9 @@
"Id": "7.2.1.28",
"Description": "Checks if the Google AI Platform Notebooks Root Access setting is enabled for AI Platform Notebook instances. The rule is NON_COMPLIANT if the Root Access setting is set to 'Enabled' for an AI Platform Notebook instance.",
"Name": "Google AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_root_access_disabled"
],
"Attributes": [
{
"Section": "7.2.1: Access to system components and data is appropriately defined and assigned.",
@@ -16153,7 +16169,9 @@
"Id": "7.2.2.28",
"Description": "Checks if the Google AI Platform Notebook instance has root access enabled. The rule is NON_COMPLIANT if the root access is set to 'Enabled' for a Google AI Platform Notebook instance.",
"Name": "Google AI Platform",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_root_access_disabled"
],
"Attributes": [
{
"Section": "7.2.2: Access to system components and data is appropriately defined and assigned.",
@@ -16282,7 +16300,9 @@
"Id": "7.2.3.9",
"Description": "Checks if the Vertex AI Workbench Root Access setting is enabled for Vertex AI Workbench Notebooks. The rule is NON_COMPLIANT if the Root Access setting is set to 'Enabled' for a Vertex AI Workbench Notebook instance.",
"Name": "Vertex AI",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_root_access_disabled"
],
"Attributes": [
{
"Section": "7.2.3: Access to system components and data is appropriately defined and assigned.",
@@ -20547,7 +20567,9 @@
"Id": "A1.1.2.21",
"Description": "Checks if direct internet access is disabled for a Google AI Platform Notebook instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "A1.1.2: Multi-tenant service providers protect and separate all customer environments and data.",
@@ -21060,7 +21082,9 @@
"Id": "A1.1.3.37",
"Description": "Checks if direct internet access is disabled for a Google Cloud AI Platform Notebook instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google Cloud AI Platform",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "A1.1.3: Multi-tenant service providers protect and separate all customer environments and data.",
@@ -22180,7 +22204,9 @@
"Id": "A3.4.1.23",
"Description": "Checks if direct internet access is disabled for a Google Cloud AI Platform Notebook instance. The rule is NON_COMPLIANT if an AI Platform Notebook instance is internet-enabled.",
"Name": "Google Cloud AI Platform Notebooks",
"Checks": [],
"Checks": [
"vertexai_workbench_instance_public_ip_disabled"
],
"Attributes": [
{
"Section": "A3.4.1: Logical access to the cardholder data environment is controlled and managed.",
@@ -81,7 +81,8 @@
"compute_instance_shielded_vm_enabled",
"compute_project_os_login_enabled",
"artifacts_container_analysis_enabled",
"gcr_container_scanning_enabled"
"gcr_container_scanning_enabled",
"appengine_version_php_runtime_supported"
]
},
{
+1 -1
View File
@@ -52,7 +52,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.45.0"
prowler_version = "5.46.1"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
+28 -1
View File
@@ -139,6 +139,28 @@ aws:
# 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:
@@ -550,8 +572,9 @@ azure:
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
python_latest_version: "3.12"
# azure.app_ensure_java_version_is_latest
@@ -650,6 +673,10 @@ gcp:
# GCP Secret Manager Rotation Period
# gcp.secretmanager_secret_rotation_enabled
secretmanager_max_rotation_days: 90
# GCP App Engine PHP runtime
# gcp.appengine_version_php_runtime_supported
# Oldest PHP branch still receiving security fixes
appengine_php_min_version: "8.2"
# Kubernetes Configuration
kubernetes:
+22
View File
@@ -88,6 +88,8 @@ _validate_trusted_ips = validate_ip_networks
_validate_semver = make_dotted_version_validator(3, 3)
# "1.28" style strings (EKS minor versions).
_validate_eks_minor = make_dotted_version_validator(2, 2)
_validate_php_branch = make_dotted_version_validator(2, 2)
_validate_platform_version = make_dotted_version_validator(3, 3)
def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]:
@@ -309,6 +311,20 @@ class AWSProviderConfig(ProviderConfigBase):
),
)
# --- Elastic Beanstalk ------------------------------------------------
elasticbeanstalk_php_min_branch: Annotated[
Optional[str], AfterValidator(_validate_php_branch)
] = Field(
default=None,
description='Oldest PHP branch still receiving security fixes, e.g. "8.2".',
)
elasticbeanstalk_php_min_platform_version: Annotated[
Optional[str], AfterValidator(_validate_platform_version)
] = Field(
default=None,
description='Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release, e.g. "4.13.13".',
)
# --- Lambda -----------------------------------------------------------
obsolete_lambda_runtimes: Optional[list[str]] = None
lambda_min_azs: Optional[int] = Field(
@@ -318,6 +334,12 @@ class AWSProviderConfig(ProviderConfigBase):
description="Min number of AZs a VPC-bound Lambda must span. Range: 1..6.",
)
# --- Lightsail --------------------------------------------------------
lightsail_php_blueprints: Optional[list[str]] = Field(
default=None,
description="Blueprint id prefixes that ship PHP and require manual runtime verification.",
)
# --- Organizations ----------------------------------------------------
organizations_enabled_regions: Optional[list[str]] = None
organizations_trusted_delegated_administrators: Annotated[
+1 -1
View File
@@ -36,7 +36,7 @@ class AzureProviderConfig(ProviderConfigBase):
# --- App Service ----------------------------------------------------
php_latest_version: Annotated[
Optional[str], AfterValidator(_validate_dotted_version)
] = Field(default=None, description='PHP minimum acceptable version, e.g. "8.2".')
] = Field(default=None, description='PHP minimum acceptable version, e.g. "8.5".')
python_latest_version: Annotated[
Optional[str], AfterValidator(_validate_dotted_version)
] = Field(
+13 -2
View File
@@ -1,10 +1,13 @@
"""GCP provider config schema with safety bounds."""
from typing import Optional
from typing import Annotated, Optional
from pydantic import Field
from pydantic import AfterValidator, Field
from prowler.config.schema.base import ProviderConfigBase
from prowler.config.schema.validators import make_dotted_version_validator
_validate_php_branch = make_dotted_version_validator(2, 2)
class GCPProviderConfig(ProviderConfigBase):
@@ -43,3 +46,11 @@ class GCPProviderConfig(ProviderConfigBase):
le=3650,
description="Min retention period on Cloud Storage buckets. Range: 1..3650.",
)
# --- App Engine -----------------------------------------------------
appengine_php_min_version: Annotated[
Optional[str], AfterValidator(_validate_php_branch)
] = Field(
default=None,
description='Oldest PHP branch still receiving security fixes, e.g. "8.2".',
)
+2
View File
@@ -12,6 +12,7 @@ from pydantic.v1 import BaseModel, Field, ValidationError, validator
from pydantic.v1.error_wrappers import ErrorWrapper
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.check.redact import redact_sensitive_fields
from prowler.lib.check.utils import recover_checks_from_provider
from prowler.lib.logger import logger
from prowler.providers.common.provider import Provider as ProviderABC
@@ -747,6 +748,7 @@ class Check_Report:
f"Resource metadata {type(resource)} in {self.check_metadata.CheckID} could not be converted to dict"
)
self.resource = {}
self.resource = redact_sensitive_fields(resource, self.resource)
self.status_extended = ""
self.resource_details = ""
self.resource_tags = getattr(resource, "tags", []) if resource else []
+43
View File
@@ -0,0 +1,43 @@
from typing import Any
REDACTED_VALUE = "REDACTED"
def _mask(value: Any) -> Any:
"""Replace every leaf of value with REDACTED_VALUE, keeping dict keys and list lengths."""
if value is None or value == "" or value == [] or value == {}:
return value
if isinstance(value, dict):
return {key: _mask(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_mask(item) for item in value]
return REDACTED_VALUE
def redact_sensitive_fields(resource: Any, data: Any) -> Any:
"""Return data, the serialised form of resource, with the fields each model lists in sensitive_fields masked."""
if isinstance(data, dict):
if isinstance(resource, dict):
return {
key: redact_sensitive_fields(resource.get(key), item)
for key, item in data.items()
}
sensitive = getattr(type(resource), "sensitive_fields", ())
return {
key: (
_mask(item)
if key in sensitive
else redact_sensitive_fields(getattr(resource, key, None), item)
)
for key, item in data.items()
}
if (
isinstance(data, list)
and isinstance(resource, (list, tuple))
and len(data) == len(resource)
):
return [
redact_sensitive_fields(item_resource, item)
for item_resource, item in zip(resource, data)
]
return data
@@ -25,7 +25,7 @@ from prowler.lib.check.compliance_config_eval import (
)
from prowler.lib.check.compliance_models import ComplianceFramework
from prowler.lib.logger import logger
from prowler.lib.outputs.utils import unroll_dict_to_list
from prowler.lib.outputs.utils import is_plain_json, unroll_dict_to_list
from prowler.lib.utils.utils import open_file
if TYPE_CHECKING:
@@ -58,6 +58,12 @@ def _sanitize_resource_data(resource_details, resource_metadata) -> dict:
return [_make_serializable(v) for v in obj]
return obj
if is_plain_json(resource_metadata):
return {
"details": resource_details,
"metadata": resource_metadata,
}
try:
converted = _make_serializable(resource_metadata)
sanitized_metadata = json.loads(json.dumps(converted, default=str))
@@ -193,6 +199,15 @@ class OCSFComplianceOutput:
for finding in findings:
if finding.check_id in check_req_map:
try:
resource_data = _sanitize_resource_data(
finding.resource_details, finding.resource_metadata
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
continue
for req in check_req_map[finding.check_id]:
cf = self._build_compliance_finding(
finding,
@@ -200,6 +215,7 @@ class OCSFComplianceOutput:
req,
compliance_name,
requirement_config_status.get(req.id, (True, "")),
resource_data=resource_data,
)
if cf:
self._data.append(cf)
@@ -255,8 +271,13 @@ class OCSFComplianceOutput:
requirement,
compliance_name: str,
config_status: tuple = (True, ""),
resource_data: dict | None = None,
) -> ComplianceFinding:
try:
if resource_data is None:
resource_data = _sanitize_resource_data(
finding.resource_details, finding.resource_metadata
)
effective_status, message = apply_config_status(
finding.status, finding.status_extended, config_status
)
@@ -348,10 +369,7 @@ class OCSFComplianceOutput:
if finding.provider == "kubernetes"
else None
),
data=_sanitize_resource_data(
finding.resource_details,
finding.resource_metadata,
),
data=resource_data,
)
],
severity_id=finding_severity.value,
+26 -2
View File
@@ -21,6 +21,8 @@ from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute
from prowler.providers.common.provider import Provider
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
RESOURCE_METADATA_WARNING_SIZE = 10 * 1024 * 1024
class Finding(BaseModel):
"""
@@ -601,8 +603,28 @@ class Finding(BaseModel):
)
raise error
@staticmethod
def _load_resource_metadata(resource, metadata_cache: dict | None) -> dict:
"""Parse the stored metadata once per resource when a cache is given."""
if metadata_cache is None:
return json.loads(resource.metadata)
if resource.id in metadata_cache:
return metadata_cache[resource.id]
metadata_size = len(resource.metadata)
if metadata_size > RESOURCE_METADATA_WARNING_SIZE:
logger.warning(
f"Resource {resource.id} ({resource.uid}) has {metadata_size} characters of metadata"
)
metadata = json.loads(resource.metadata)
metadata_cache[resource.id] = metadata
return metadata
@classmethod
def transform_api_finding(cls, finding, provider) -> "Finding":
def transform_api_finding(
cls, finding, provider, metadata_cache: dict | None = None
) -> "Finding":
"""
Transform a FindingModel instance into an API-friendly Finding object.
@@ -616,6 +638,8 @@ class Finding(BaseModel):
Args:
finding (API Finding): An API Finding instance containing data from the database.
provider (Provider): the provider object.
metadata_cache (dict | None): parsed resource metadata keyed by resource id,
shared by the findings of a batch so each resource is parsed once.
Returns:
Finding: A new Finding instance populated with data from the provided model.
@@ -624,7 +648,7 @@ class Finding(BaseModel):
resource = finding.resources.first()
finding.resource_arn = resource.uid
finding.resource_name = resource.name
finding.resource = json.loads(resource.metadata)
finding.resource = cls._load_resource_metadata(resource, metadata_cache)
finding.resource_details = resource.details
finding.resource_id = resource.name if provider.type == "aws" else resource.uid
+7 -1
View File
@@ -27,7 +27,7 @@ from uuid6 import UUID
from prowler.lib.logger import logger
from prowler.lib.outputs.finding import Finding
from prowler.lib.outputs.output import Output
from prowler.lib.outputs.utils import unroll_dict_to_list
from prowler.lib.outputs.utils import is_plain_json, unroll_dict_to_list
class OCSF(Output):
@@ -279,6 +279,12 @@ class OCSF(Output):
return [_make_serializable(v) for v in obj]
return obj
if is_plain_json(resource_metadata):
return {
"details": resource_details,
"metadata": resource_metadata,
}
try:
converted = _make_serializable(resource_metadata)
sanitized_metadata = json.loads(json.dumps(converted, default=str))
+17
View File
@@ -220,3 +220,20 @@ def sanitize_csv_value(value):
if isfinite(number) and value == value.strip():
return value
return f"'{value}"
def is_plain_json(value) -> bool:
"""Return True when value is made only of JSON types with str keys."""
try:
if value is None or isinstance(value, (str, int, float, bool)):
return True
if isinstance(value, dict):
return all(
isinstance(key, str) and is_plain_json(item)
for key, item in value.items()
)
if isinstance(value, list):
return all(is_plain_json(item) for item in value)
return False
except RecursionError:
return False
@@ -126,6 +126,7 @@
"regions": {
"aws": [
"ap-southeast-1",
"us-east-1",
"us-west-1"
],
"aws-cn": [],
@@ -262,8 +263,12 @@
"aws": [
"ap-northeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
@@ -1517,8 +1522,10 @@
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"sa-east-1",
"us-east-1",
"us-west-2"
@@ -1924,7 +1931,10 @@
"us-west-1",
"us-west-2"
],
"aws-cn": [],
"aws-cn": [
"cn-north-1",
"cn-northwest-1"
],
"aws-eusc": [],
"aws-iso": [],
"aws-iso-b": [],
@@ -1938,6 +1948,7 @@
"aws": [
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
@@ -3107,7 +3118,8 @@
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": [
"us-gov-east-1"
"us-gov-east-1",
"us-gov-west-1"
]
}
},
@@ -3303,7 +3315,8 @@
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": [
"us-gov-east-1"
"us-gov-east-1",
"us-gov-west-1"
]
}
},
@@ -4262,7 +4275,10 @@
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
"aws-us-gov": [
"us-gov-east-1",
"us-gov-west-1"
]
}
},
"deadline-cloud": {
@@ -5826,6 +5842,48 @@
]
}
},
"endusermessaging": {
"regions": {
"aws": [
"af-south-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-6",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2"
],
"aws-cn": [],
"aws-eusc": [],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
}
},
"entityresolution": {
"regions": {
"aws": [
@@ -5972,6 +6030,18 @@
]
}
},
"eventbridgev2": {
"regions": {
"aws": [],
"aws-cn": [],
"aws-eusc": [],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
}
},
"events": {
"regions": {
"aws": [
@@ -6745,18 +6815,23 @@
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -7465,6 +7540,20 @@
]
}
},
"interconnect": {
"regions": {
"aws": [
"eu-south-1"
],
"aws-cn": [],
"aws-eusc": [],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
}
},
"internetmonitor": {
"regions": {
"aws": [
@@ -8058,7 +8147,9 @@
"cn-north-1",
"cn-northwest-1"
],
"aws-eusc": [],
"aws-eusc": [
"eusc-de-east-1"
],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
@@ -9952,6 +10043,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -10013,6 +10105,7 @@
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": [
"us-gov-east-1",
"us-gov-west-1"
]
}
@@ -10175,6 +10268,35 @@
]
}
},
"network-security-manager": {
"regions": {
"aws": [
"ap-east-1",
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-5",
"ap-southeast-7",
"eu-central-1",
"eu-north-1",
"eu-south-2",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-eusc": [
"eusc-de-east-1"
],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
}
},
"networkflowmonitor": {
"regions": {
"aws": [
@@ -10280,7 +10402,6 @@
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -10497,18 +10618,22 @@
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-4",
"ca-central-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2"
],
"aws-cn": [],
@@ -11015,10 +11140,12 @@
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"eu-central-1",
"eu-north-1",
@@ -13473,6 +13600,10 @@
"securityagent": {
"regions": {
"aws": [
"ap-northeast-2",
"ca-central-1",
"eu-north-1",
"eu-west-2",
"us-east-1",
"us-west-2"
],
@@ -14913,7 +15044,39 @@
},
"supportauthz": {
"regions": {
"aws": [],
"aws": [
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2"
],
"aws-cn": [],
"aws-eusc": [],
"aws-iso": [],
@@ -15089,12 +15252,14 @@
"textract": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-central-2",
"eu-south-2",
"eu-west-1",
"eu-west-2",
@@ -15146,10 +15311,8 @@
"aws": [
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"us-east-1",
"us-east-2",
@@ -16047,6 +16210,22 @@
]
}
},
"wellarchitectedagent": {
"regions": {
"aws": [
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-eusc": [],
"aws-iso": [],
"aws-iso-b": [],
"aws-iso-e": [],
"aws-iso-f": [],
"aws-us-gov": []
}
},
"wellarchitectedtool": {
"regions": {
"aws": [
@@ -1,3 +1,5 @@
from typing import ClassVar
from botocore.exceptions import ClientError
from pydantic.v1 import BaseModel, Field
@@ -11,6 +13,7 @@ class Branch(BaseModel):
name: str
arn: str
sensitive_fields: ClassVar[tuple[str, ...]] = ("environment_variables",)
environment_variables: dict = Field(default_factory=dict)
@@ -21,6 +24,10 @@ class App(BaseModel):
name: str
arn: str
region: str
sensitive_fields: ClassVar[tuple[str, ...]] = (
"environment_variables",
"build_spec",
)
environment_variables: dict = Field(default_factory=dict)
build_spec: str = ""
branches: list[Branch] = Field(default_factory=list)
@@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any, ClassVar, Optional
from botocore.exceptions import ClientError
from pydantic.v1 import BaseModel
@@ -266,6 +266,7 @@ class Stage(BaseModel):
tracing_enabled: Optional[bool] = None
cache_enabled: Optional[bool] = None
cache_data_encrypted: Optional[bool] = None
sensitive_fields: ClassVar[tuple[str, ...]] = ("variables",)
variables: Optional[dict] = {}
@@ -1,3 +1,5 @@
from typing import ClassVar
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
@@ -155,6 +157,7 @@ class ApplicationAutoScaling(AWSService):
class LaunchConfiguration(BaseModel):
arn: str
name: str
sensitive_fields: ClassVar[tuple[str, ...]] = ("user_data",)
user_data: str
image_id: str
region: str
@@ -3,7 +3,7 @@ import json
import zipfile
from concurrent.futures import as_completed
from enum import Enum
from typing import Any, Optional
from typing import Any, ClassVar, Optional
import requests
from botocore.client import ClientError
@@ -404,6 +404,7 @@ class Function(BaseModel):
security_groups: list
last_modified: Optional[str] = None
runtime: Optional[str] = None
sensitive_fields: ClassVar[tuple[str, ...]] = ("environment",)
environment: Optional[dict] = None
region: str
policy: dict = {}
@@ -1,5 +1,5 @@
from itertools import zip_longest
from typing import Optional
from typing import ClassVar, Optional
from botocore.exceptions import ClientError
from pydantic.v1 import BaseModel
@@ -14,6 +14,7 @@ class ContainerEnvVariable(BaseModel):
"""An environment variable name-value pair."""
name: str
sensitive_fields: ClassVar[tuple[str, ...]] = ("value",)
value: str
@@ -21,6 +22,7 @@ class BatchContainerProperties(BaseModel):
"""Container properties for an AWS Batch job definition."""
image: Optional[str]
sensitive_fields: ClassVar[tuple[str, ...]] = ("command",)
command: list[str] = []
environment: list[ContainerEnvVariable] = []
@@ -19,18 +19,33 @@ class bedrock_api_key_no_long_term_credentials(Check):
FAIL (high) for any other active long-term key.
"""
def execute(self):
def execute(self) -> list[Check_Report_AWS]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for api_key in iam_client.service_specific_credentials:
if api_key.service_name != "bedrock.amazonaws.com":
continue
if not api_key.expiration_date:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=api_key)
now = datetime.now(timezone.utc)
if api_key.expiration_date <= now:
if api_key.expiration_date is None:
# ExpirationDate "is only present for Bedrock API keys ... that were created with
# an expiration period" (IAM ServiceSpecificCredentialMetadata), so its absence is
# the never-expiring key itself, not a key outside this check's scope.
report.status = "FAIL"
report.check_metadata.Severity = Severity.critical
report.status_extended = (
f"Bedrock long-term API key {api_key.id} in user "
f"{api_key.user.name} has no expiration date, so it never expires. "
f"Use short-term Bedrock API keys (session-scoped, valid up to "
f"12 hours) for non-exploratory workloads instead."
)
elif api_key.expiration_date <= now:
report.status = "PASS"
report.status_extended = (
f"Bedrock long-term API key {api_key.id} in user "
@@ -0,0 +1,46 @@
{
"Provider": "aws",
"CheckID": "bedrock_guardrail_sensitive_information_filter_enforced",
"CheckTitle": "Amazon Bedrock guardrail sensitive information filter acts on the output path",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis",
"Effects/Data Exposure",
"Sensitive Data Identifications/PII"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "A **Bedrock guardrail** sensitive information entry whose action is `NONE` reports the detected PII or regex match in the trace and still delivers it to the caller. The evaluation looks for guardrails whose configured PII entities and custom regexes are set to `BLOCK` or `ANONYMIZE` on the output path.",
"Risk": "A filter that detects without acting reads as configured while **PII**, credentials and secrets keep flowing through model output.\n- Reviews and dashboards record the filter as present, so the gap goes unnoticed\n- Leaked tokens and customer records enable unauthorized access, fraud and identity theft",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html",
"https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GuardrailPiiEntity.html",
"https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GuardrailRegex.html",
"https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the AWS Management Console and open Amazon Bedrock\n2. Go to Guardrails and select <example_resource_name>\n3. Click Edit (or Open draft) and open Sensitive information filters\n4. For every PII type and regular expression listed, change the action from None to Mask or Block, and leave the output evaluation enabled\n5. Click Save, then publish a new guardrail version and point your agents and model invocations at it\nNote for API and CLI users: UpdateGuardrail replaces the whole guardrail, so it must be a read-modify-write. Call GetGuardrail first, edit only the actions inside the returned sensitiveInformationPolicyConfig, and send every other policy block back unchanged. AWS states \"Include both fields that you want to update as well as fields that you want to keep the same\" -- omitting a block deletes it. Changes land on the working draft only, so publish a new version afterwards.",
"Terraform": ""
},
"Recommendation": {
"Text": "Set every PII entity and custom regular expression in the guardrail sensitive information policy to `BLOCK` or `ANONYMIZE` rather than `NONE`, and keep output evaluation enabled so matches are stopped on the response path. Use `BLOCK` for values that must never appear, `ANONYMIZE` where the surrounding answer is still useful. Confirm the result with an `ApplyGuardrail` call using `source=OUTPUT` and check that the assessment reports the entity as blocked or anonymized.",
"Url": "https://hub.prowler.com/check/bedrock_guardrail_sensitive_information_filter_enforced"
}
},
"Categories": [
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [
"bedrock_guardrail_sensitive_information_filter_enabled"
],
"Notes": "Scoped to guardrails that already carry a sensitive information policy; a guardrail with no such policy is reported by bedrock_guardrail_sensitive_information_filter_enabled instead. GetGuardrail reports `action` for every entry and omits the per-path `outputAction` and `outputEnabled` unless they were set explicitly, in which case `action` governs the output path — verified against a live guardrail whose entries carried only `action: ANONYMIZE` and which masked PII on an ApplyGuardrail call with source=OUTPUT. An entry whose action the API does not report is MANUAL, never PASS. Like the other guardrail checks, this reads the working draft, because the collector calls GetGuardrail without guardrailVersion."
}
@@ -0,0 +1,105 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_client import bedrock_client
ENFORCING_ACTIONS = {"BLOCK", "ANONYMIZE"}
class bedrock_guardrail_sensitive_information_filter_enforced(Check):
"""Ensure guardrail sensitive-information entries block or mask matches on the output path.
A guardrail can carry a full sensitive-information policy whose every PII entity and regex is
set to NONE, which detects the match and returns it in the trace while still delivering the
value to the caller. Presence of the policy therefore does not establish that leakage is
stopped, which is all bedrock_guardrail_sensitive_information_filter_enabled asserts.
Scope: guardrails that already carry a sensitive-information policy. A guardrail with no such
policy is the subject of bedrock_guardrail_sensitive_information_filter_enabled and is not
reported here.
PASS when every entry blocks or masks its matches on the output path.
FAIL when any entry takes no action on the output path, or has output evaluation switched off.
MANUAL when the guardrail configuration could not be read, or an entry's action was not
reported AND no other entry was proven unenforced. A proven leak outranks an incomplete
inventory: this is a universal claim, so one entry known to take no action settles it.
Unreported entries are still named in the FAIL text, so an incomplete inventory is disclosed
rather than dropped.
ENFORCING_ACTIONS holds only BLOCK and ANONYMIZE. NONE reports the detection in the trace and
lets the content through, so an entry set to NONE detects a leak and permits it.
Both name lists are rendered into status_extended, and they are built in the order GetGuardrail
returned piiEntities and regexes. Neither ListGuardrails nor GetGuardrail documents an ordering,
so each render point sorts independently: unsorted output makes one unchanged guardrail produce
two different status_extended strings across scans, which reads as a changed finding.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for guardrail in bedrock_client.guardrails.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=guardrail)
if not guardrail.detail_retrieved:
report.status = "MANUAL"
report.status_extended = (
f"Bedrock Guardrail {guardrail.name} configuration could not be retrieved "
f"({guardrail.detail_error or 'unknown error'}); verify manually that its "
"sensitive information filters block or mask matches on the output path."
)
findings.append(report)
continue
if not guardrail.sensitive_information_entries:
continue
unreported = []
unenforced = []
for entry in guardrail.sensitive_information_entries:
# GetGuardrail omits outputAction unless it was configured explicitly; the
# required `action` governs the output path in that case.
action = entry.output_action or entry.action
if action is None:
unreported.append(entry.name)
elif entry.output_enabled is False or action not in ENFORCING_ACTIONS:
unenforced.append(entry.name)
if unenforced:
unenforced.sort()
unreported.sort()
report.status = "FAIL"
unreported_note = (
f" The action for {', '.join(unreported)} was not reported, so there may be "
"more."
if unreported
else ""
)
report.status_extended = (
f"Bedrock Guardrail {guardrail.name} detects but does not block or mask "
f"{', '.join(unenforced)} on the output path, so matched sensitive "
f"information still reaches the caller.{unreported_note}"
)
elif unreported:
# Sorted here too: a second render point needs its own sort, not an inherited one.
unreported.sort()
report.status = "MANUAL"
report.status_extended = (
f"Bedrock Guardrail {guardrail.name} did not report an action for "
f"{', '.join(unreported)}; verify manually that matches are blocked or "
"masked on the output path."
)
else:
report.status = "PASS"
count = len(guardrail.sensitive_information_entries)
report.status_extended = (
f"Bedrock Guardrail {guardrail.name} blocks or masks "
f"{'its 1 configured sensitive information entry' if count == 1 else f'all {count} configured sensitive information entries'}"
" on the output path."
)
findings.append(report)
return findings
@@ -0,0 +1,43 @@
{
"Provider": "aws",
"CheckID": "bedrock_model_invocation_logging_text_data_enabled",
"CheckTitle": "Bedrock model invocation logging delivers prompt and response text.",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "bedrock",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Other",
"ResourceGroup": "ai_ml",
"Description": "Bedrock model invocation logging is checked for text data delivery. A logging configuration can name a destination while excluding request and response bodies, so the destination alone does not establish that prompts and completions are captured.",
"Risk": "Invocation logging that excludes **text data** records that a call happened, but not what was sent or returned.\n\nThat leaves no evidence for the investigations which depend on content itself: **prompt injection**, data exfiltration, and abuse of the model by an authorized caller.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html#model-invocation-logging-console",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/aws/Bedrock/enable-model-invocation-logging.html",
"https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html"
],
"Remediation": {
"Code": {
"CLI": "aws bedrock put-model-invocation-logging-configuration --logging-config '{\"textDataDeliveryEnabled\":true,\"cloudWatchConfig\":{\"logGroupName\":\"<log_group>\",\"roleArn\":\"<role_arn>\"}}'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Set textDataDeliveryEnabled to true on the model invocation logging configuration so prompt and response bodies are delivered to the configured destination.",
"Url": "https://hub.prowler.com/check/bedrock_model_invocation_logging_text_data_enabled"
}
},
"Categories": [
"logging",
"forensics-ready",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,62 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.bedrock.bedrock_client import bedrock_client
class bedrock_model_invocation_logging_text_data_enabled(Check):
"""Ensure model invocation logging delivers prompt and response text.
Model invocation logging can name a CloudWatch log group or S3 bucket while excluding the
request and response bodies, so a configured destination does not establish that prompts and
completions are captured. This check is scoped to Regions where invocation logging is already
enabled: a Region with logging switched off is the subject of
bedrock_model_invocation_logging_enabled and is not reported here.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for region, logging in bedrock_client.logging_configurations.items():
# Skip-if-absent: only Regions that already deliver logs are in scope.
if not logging.enabled:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=logging)
report.region = region
# "model-invocation-logging", as both siblings on this configuration use. The account
# id identified no resource -- every Region reported the same id, and the ARN beside it
# already carries the account -- so a reader could not tell two Regions' findings apart
# by resource_id alone.
report.resource_id = "model-invocation-logging"
report.resource_arn = (
bedrock_client._get_model_invocation_logging_arn_template(region)
)
if logging.text_data_delivery_enabled is None:
# The API did not report the flag. Absent is not False, and reporting it as
# compliant would assert content capture that was never confirmed.
report.status = "MANUAL"
report.status_extended = (
f"Bedrock Model Invocation Logging is enabled in region {region} but the "
"text data delivery setting was not returned; verify manually that prompt "
"and response text is delivered."
)
elif logging.text_data_delivery_enabled:
report.status = "PASS"
report.status_extended = (
f"Bedrock Model Invocation Logging in region {region} delivers prompt and "
"response text."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Bedrock Model Invocation Logging in region {region} excludes prompt and "
"response text, so invocation content is not captured."
)
findings.append(report)
return findings
@@ -33,6 +33,13 @@ class Bedrock(AWSService):
)
def _get_model_invocation_logging_configuration(self, regional_client):
"""Read the model invocation logging configuration for a region.
An empty `loggingConfig` is how the API reports logging switched off, so it maps to
`enabled=False` rather than to an absent entry. The four per-data-type delivery flags are
stored as reported, including None: the API omits a flag it was never given, and None
cannot be collapsed to False without asserting an exclusion that was never configured.
"""
logger.info("Bedrock - Getting Model Invocation Logging Configuration...")
try:
logging_config = (
@@ -48,6 +55,18 @@ class Bedrock(AWSService):
).get("logGroupName"),
s3_bucket=logging_config.get("s3Config", {}).get("bucketName"),
enabled=True,
text_data_delivery_enabled=logging_config.get(
"textDataDeliveryEnabled"
),
image_data_delivery_enabled=logging_config.get(
"imageDataDeliveryEnabled"
),
embedding_data_delivery_enabled=logging_config.get(
"embeddingDataDeliveryEnabled"
),
video_data_delivery_enabled=logging_config.get(
"videoDataDeliveryEnabled"
),
)
)
else:
@@ -94,6 +113,17 @@ class Bedrock(AWSService):
)
def _get_guardrail(self, guardrail):
"""Read one guardrail's policies and flatten its sensitive-information entries.
`piiEntities` and `regexes` are flattened into a single list because both carry the same
action/outputAction/outputEnabled triple and both leak the same way; the entry name keeps
the kind visible in findings. Each field is stored as reported, so an action the API did
not return stays None instead of becoming a verdict.
`detail_retrieved` is set only on the success path. A guardrail whose configuration could
not be read therefore carries no filter claims at all, and `detail_error` names the API
error code so a check can report the cause rather than a compliant-looking default.
"""
logger.info("Bedrock - Getting Guardrail...")
try:
guardrail_info = self.regional_clients[guardrail.region].get_guardrail(
@@ -119,8 +149,37 @@ class Bedrock(AWSService):
"filters", []
)
]
sensitive_information_policy = guardrail_info.get(
"sensitiveInformationPolicy", {}
)
for entity in sensitive_information_policy.get("piiEntities", []):
guardrail.sensitive_information_entries.append(
SensitiveInformationEntry(
name=f"PII entity {entity.get('type', 'unknown')}",
action=entity.get("action"),
output_action=entity.get("outputAction"),
output_enabled=entity.get("outputEnabled"),
)
)
for regex in sensitive_information_policy.get("regexes", []):
guardrail.sensitive_information_entries.append(
SensitiveInformationEntry(
name=f"regex {regex.get('name', 'unknown')}",
action=regex.get("action"),
output_action=regex.get("outputAction"),
output_enabled=regex.get("outputEnabled"),
)
)
guardrail.detail_retrieved = True
except ClientError as error:
guardrail.detail_error = error.response["Error"].get(
"Code", error.__class__.__name__
)
logger.error(
f"{guardrail.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
guardrail.detail_error = error.__class__.__name__
logger.error(
f"{guardrail.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@@ -201,6 +260,29 @@ class LoggingConfiguration(BaseModel):
enabled: bool = False
cloudwatch_log_group: Optional[str] = None
s3_bucket: Optional[str] = None
# GetModelInvocationLoggingConfiguration reports which data types are actually delivered.
# A configuration can name a destination while excluding the prompt and response bodies, so
# the destination alone does not establish that invocation content is captured. None means
# the API did not report the flag, which is not the same as False.
text_data_delivery_enabled: Optional[bool] = None
image_data_delivery_enabled: Optional[bool] = None
embedding_data_delivery_enabled: Optional[bool] = None
video_data_delivery_enabled: Optional[bool] = None
class SensitiveInformationEntry(BaseModel):
"""One entry of a guardrail sensitive-information policy: a PII entity or a custom regex.
GetGuardrail always reports `action` (the API marks it required) and omits the per-path
`outputAction` / `outputEnabled` unless they were configured explicitly, in which case
`action` governs both the input and the output path. None therefore means the API did not
report the per-path setting, which is not the same as False or NONE.
"""
name: str
action: Optional[str] = None
output_action: Optional[str] = None
output_enabled: Optional[bool] = None
class ContextualGroundingFilter(BaseModel):
@@ -227,6 +309,8 @@ class Guardrail(BaseModel):
contextual_grounding_filters: list[ContextualGroundingFilter] = []
# False when GetGuardrail failed: absent policy is unknown, not unset.
detail_retrieved: bool = False
detail_error: Optional[str] = None
sensitive_information_entries: list[SensitiveInformationEntry] = []
class CustomModel(BaseModel):
@@ -1,4 +1,4 @@
from typing import Optional
from typing import ClassVar, Optional
from botocore.client import ClientError
from pydantic.v1 import BaseModel
@@ -82,6 +82,7 @@ class Stack(BaseModel):
"""In the CloudFormation API the "Stacks[].StackId" is the ARN"""
name: str
"""Stacks[].StackName"""
sensitive_fields: ClassVar[tuple[str, ...]] = ("outputs",)
outputs: list[str]
"""Stacks[].Outputs"""
enable_termination_protection: bool = False
@@ -1,6 +1,6 @@
import json
from datetime import datetime, timezone
from typing import Optional
from typing import ClassVar, Optional
from botocore.exceptions import ClientError
from pydantic.v1 import BaseModel
@@ -381,6 +381,7 @@ class LogGroup(BaseModel):
# Properties inherited from account-level settings, e.g. ACCOUNT_DATA_PROTECTION.
inherited_properties: list[str] = []
region: str
sensitive_fields: ClassVar[tuple[str, ...]] = ("log_streams",)
log_streams: dict[str, list[str]] = (
{}
) # Log stream name as the key, array of events as the value
@@ -1,6 +1,6 @@
import datetime
from concurrent.futures import as_completed
from typing import List, Optional
from typing import ClassVar, List, Optional
from pydantic.v1 import BaseModel
@@ -286,6 +286,7 @@ class Source(BaseModel):
class EnvironmentVariable(BaseModel):
name: str
sensitive_fields: ClassVar[tuple[str, ...]] = ("value",)
value: str
type: str
Loaded 100 of 506 files, more files were not shown because too many files have changed in this diff. Show more