diff --git a/.env b/.env
index 99ec6b55d2..03bc3dc7a4 100644
--- a/.env
+++ b/.env
@@ -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.0
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml
index 38cbfb6253..19cc3cd217 100644
--- a/.github/workflows/find-secrets.yml
+++ b/.github/workflows/find-secrets.yml
@@ -38,13 +38,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- # PRs only need the diff range; push to master/release walks the new range from event.before.
- # 50 is enough headroom for the longest realistic PR/push chain without paying for a full clone.
- fetch-depth: 50
+ # Full history: a shallow clone cuts the PR's ancestry, git can no longer exclude commits
+ # already on the base branch, and TruffleHog scans the cut-off commit as a root (the whole
+ # tree as added). PRs that merge master in hit this and fail on pre-existing content.
+ fetch-depth: 0
persist-credentials: false
- name: Scan diff for secrets with TruffleHog
# Action auto-injects --since-commit/--branch from event payload; passing them in extra_args produces duplicate flags.
- uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3.95.3
+ uses: trufflesecurity/trufflehog@e76dff8a7e57d6df0c60ad8cbf5d5cba505a8fc9 # v3.99.2
with:
+ # The action runs the `latest` image unless told otherwise: pin the scanner to the action's release.
+ version: 3.99.2@sha256:47a84bc18a0d04a165498bbbd3bacfd84176661cbc91f8e0f85467f6a771e99a
extra_args: --results=verified,unknown
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 65809f15f0..238c126802 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -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
diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml
index 207a1f9b16..2492fe3d49 100644
--- a/.github/workflows/sdk-tests.yml
+++ b/.github/workflows/sdk-tests.yml
@@ -640,6 +640,30 @@ jobs:
flags: prowler-py${{ matrix.python-version }}-e2enetworks
files: ./e2enetworks_coverage.xml
+ # Huawei Cloud Provider
+ - name: Check if Huawei Cloud files changed
+ if: steps.check-changes.outputs.any_changed == 'true'
+ id: changed-huaweicloud
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
+ with:
+ files: |
+ ./prowler/**/huaweicloud/**
+ ./tests/**/huaweicloud/**
+ ./uv.lock
+
+ - name: Run Huawei Cloud tests
+ if: steps.changed-huaweicloud.outputs.any_changed == 'true'
+ run: uv run pytest -n auto --cov=./prowler/providers/huaweicloud --cov-report=xml:huaweicloud_coverage.xml tests/providers/huaweicloud
+
+ - name: Upload Huawei Cloud coverage to Codecov
+ if: steps.changed-huaweicloud.outputs.any_changed == 'true'
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ flags: prowler-py${{ matrix.python-version }}-huaweicloud
+ files: ./huaweicloud_coverage.xml
+
# External Provider (dynamic loading)
- name: Check if External Provider files changed
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.grype.yaml b/.grype.yaml
index 81e6be2eb9..18345342e8 100644
--- a/.grype.yaml
+++ b/.grype.yaml
@@ -7,66 +7,19 @@
ignore:
- # Modules compiled into the Trivy binary we ship.
- # Only a Trivy rebuild by its vendor can change these; the version is pinned in our Dockerfile.
- # CVE-2026-71556 is the same temporary exception documented in .trivyignore.yaml:
- # Trivy 0.73.0 still embeds go-git 5.19.1, while the 5.19.2 fix is merged only on
- # Trivy main. Remove this entry with the Trivy exception by 2026-09-15.
- # https://github.com/aquasecurity/trivy/blob/v0.73.0/go.mod#L46
- # https://github.com/aquasecurity/trivy/commit/a2edba9a03987ba0d2ebc8212c1a9a1e6979497b
- - vulnerability: CVE-2026-71556
- package:
- name: github.com/go-git/go-git/v5
- # CVE-2026-84304 is the same temporary exception documented in .trivyignore.yaml:
- # Trivy 0.74.0 still embeds grpc 1.82.1, while the 1.83.1 fix is not in any release.
- # Prowler only runs `trivy image` / `trivy fs`, never client/server mode, so no gRPC
- # endpoint exists in the image. Pinned to the embedded version so the rule stops
- # matching on its own once Trivy bumps grpc. Remove with the Trivy exception by 2026-10-15.
- # https://github.com/aquasecurity/trivy/pull/11176
- - vulnerability: CVE-2026-84304
- package:
- name: google.golang.org/grpc
- version: v1.82.1
- # CVE-2026-84445 is the same temporary exception documented in .trivyignore.yaml:
- # Trivy 0.74.0 still embeds grpc 1.82.1, while the 1.82.2 / 1.83.2 fix is not in any
- # release. The panic needs a gRPC server built with `xds.NewGRPCServer()`; Prowler only
- # runs `trivy image` / `trivy fs`, so the image serves no gRPC at all. Pinned to the
- # embedded version so the rule stops matching on its own once Trivy bumps grpc. Remove
- # with the Trivy exception by 2026-10-15.
- # https://github.com/advisories/GHSA-2v4p-qf9q-27wj
- - vulnerability: CVE-2026-84445
- package:
- name: google.golang.org/grpc
- version: v1.82.1
- # CVE-2026-56855 / CVE-2026-78662 are the same temporary exception documented in
- # .trivyignore.yaml: Trivy 0.74.0 still embeds golang.org/x/crypto v0.55.0, while the
- # 0.56.0 fix (published 2026-09-02) hasn't reached any Trivy release, or even Trivy
- # main, yet. Pinned to the embedded version so the rule stops matching on its own once
- # Trivy bumps it. Remove with the Trivy exception by 2026-10-15.
- - vulnerability: CVE-2026-56855
- package:
- name: golang.org/x/crypto
- version: v0.55.0
- - vulnerability: CVE-2026-78662
- package:
- name: golang.org/x/crypto
- version: v0.55.0
- - vulnerability: CVE-2026-56852
- package:
- name: golang.org/x/text
- - vulnerability: GHSA-hrxh-6v49-42gf
- package:
- name: google.golang.org/grpc
- - vulnerability: CVE-2026-50151
- package:
- name: oras.land/oras-go/v2
-
# Shipped inside the PowerShell tarball, in its bundled MicrosoftTeams module.
# Not a dependency we declare, and not one we can upgrade independently.
- vulnerability: CVE-2026-26127
package:
name: Microsoft.Bcl.Memory
+ # Documented in .trivyignore.yaml: ships inside ExchangeOnlineManagement
+ # 3.10.1, the latest gallery release. Re-check on the next EXO release.
+ - vulnerability: CVE-2026-84008
+ package:
+ name: System.Security.Cryptography.Pkcs
+ version: 10.0.0
+
# The .NET runtime bundled inside the PowerShell tarball the Dockerfile pins.
# CVE-2026-62901 is the same temporary exception documented in .trivyignore.yaml:
# fixed in .NET 9.0.19 / 10.0.11 (2026-08-11), but no published PowerShell release
diff --git a/.trivyignore.yaml b/.trivyignore.yaml
index 3683a23e0a..c13bd5f114 100644
--- a/.trivyignore.yaml
+++ b/.trivyignore.yaml
@@ -108,6 +108,23 @@ vulnerabilities:
purls:
- "pkg:nuget/System.Security.Cryptography.Xml"
expired_at: 2027-01-31
+ # Same manifest, pinned to 8.0.4: PowerShell's own copy is also flagged by these CVEs.
+ - id: CVE-2026-96222
+ purls:
+ - "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
+ expired_at: 2027-01-31
+ - id: CVE-2026-96958
+ purls:
+ - "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
+ expired_at: 2027-01-31
+ - id: CVE-2026-96959
+ purls:
+ - "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
+ expired_at: 2027-01-31
+ - id: CVE-2026-96960
+ purls:
+ - "pkg:nuget/System.Security.Cryptography.Xml@8.0.4"
+ expired_at: 2027-01-31
- id: CVE-2026-13676
purls:
- "pkg:npm/fast-uri"
@@ -181,41 +198,12 @@ vulnerabilities:
- "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
+ # Unlike the manifest entries above, this DLL ships in the image:
+ # ExchangeOnlineManagement/3.10.1/netCore/System.Security.Cryptography.Pkcs.dll. It is a
+ # DoS in CMS/PKCS parsing; Prowler only feeds it the tenant's own certificate. 3.10.1 is
+ # the latest gallery release, so only Microsoft can update it. Re-check on the next EXO
+ # release.
+ - id: CVE-2026-84008
purls:
- - "pkg:golang/stdlib@v1.27.1"
- expired_at: 2026-11-15
- - id: CVE-2026-97031
- purls:
- - "pkg:golang/stdlib@v1.27.1"
- expired_at: 2026-11-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
+ - "pkg:nuget/System.Security.Cryptography.Pkcs@10.0.0"
+ expired_at: 2026-12-15
diff --git a/Dockerfile b/Dockerfile
index 1a1ad42c7e..a9b5237fad 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,22 +3,22 @@ FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
-ARG POWERSHELL_VERSION=7.5.11
+ARG POWERSHELL_VERSION=7.5.12
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.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
-ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
-ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
-ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
-ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
+ARG TRIVY_SHA256_AMD64=e24b1c51293c1b15e4ba8d45bbc7891ba053f90278ec2698f3562c993e145fe7
+ARG TRIVY_SHA256_ARM64=aab21e677a7b7071fc79f1711480840754c18a1b9d9cefa7ccd06af63a0e8f4c
+ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
+ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index afd6c2d86d..4a0d6037ad 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -4,6 +4,28 @@ All notable changes to the **Prowler API** are documented in this file.
+## [1.46.0] (Prowler v5.45.0)
+
+### 🐞 Fixed
+
+- OCI provider secrets keep the region as home region for credential validation and scans, instead of always using us-ashburn-1 [(#12865)](https://github.com/prowler-cloud/prowler/pull/12865)
+- Gunicorn workers are now recycled after 10000 requests (plus up to 1000 of jitter) to cap memory growth, configurable with `GUNICORN_MAX_REQUESTS` and `GUNICORN_MAX_REQUESTS_JITTER` [(#13047)](https://github.com/prowler-cloud/prowler/pull/13047)
+
+### 🔐 Security
+
+- `pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9 [(#12911)](https://github.com/prowler-cloud/prowler/pull/12911)
+- Sentry error events no longer include user identity, IP addresses, cookies, headers or request bodies, which could contain personal data or provider credentials [(#12912)](https://github.com/prowler-cloud/prowler/pull/12912)
+- Kubernetes kubeconfig validation accepts only inline credentials and known cluster fields, rejecting file-path fields (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), command-based authentication (`exec`, `cmd-path`), `auth-provider` directives, proxy URLs and unknown user or cluster keys; stored kubeconfigs are re-validated before each scan and connection test [(#12952)](https://github.com/prowler-cloud/prowler/pull/12952)
+- `django` from 5.1.15 to 5.2.17, patching CVE-2026-15307 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
+- `dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
+- `urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
+- `pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749 [(#12955)](https://github.com/prowler-cloud/prowler/pull/12955)
+- `GET /api/v1/users/{id}` no longer exposes another user's memberships, roles or tenant names from tenants other than the one in the request token [(#12965)](https://github.com/prowler-cloud/prowler/pull/12965)
+- Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the API container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy [(#13009)](https://github.com/prowler-cloud/prowler/pull/13009)
+- PowerShell from 7.5.11 to 7.5.12 in the API container image, bundling .NET runtime 9.0.21 [(#13037)](https://github.com/prowler-cloud/prowler/pull/13037)
+
+---
+
## [1.45.0] (Prowler v5.44.0)
### 🚀 Added
diff --git a/api/Dockerfile b/api/Dockerfile
index d19c3c9474..ab954829f3 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -2,22 +2,22 @@ FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4
LABEL maintainer="https://github.com/prowler-cloud/api"
-ARG POWERSHELL_VERSION=7.5.11
+ARG POWERSHELL_VERSION=7.5.12
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.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
ENV ZIZMOR_VERSION=${ZIZMOR_VERSION}
# Pinned here, not fetched with the artefact: a compromised release ships its own checksum.
-ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f
-ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3
-ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8
-ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672
+ARG TRIVY_SHA256_AMD64=e24b1c51293c1b15e4ba8d45bbc7891ba053f90278ec2698f3562c993e145fe7
+ARG TRIVY_SHA256_ARM64=aab21e677a7b7071fc79f1711480840754c18a1b9d9cefa7ccd06af63a0e8f4c
+ARG POWERSHELL_SHA256_AMD64=db85ae1f45005ffd20e456890585167257151fa99406ccd7ab344f7cf3a766fd
+ARG POWERSHELL_SHA256_ARM64=29eb382cd510e4bd246902650ff6cf9f3b56f959d1bd0be75e0a1e425177c831
ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03
ARG ZIZMOR_SHA256_ARM64=d66e37ef8a375fb07939c630ebf9709a6e0f20242bdc3faf672a7ed97e0b768d
diff --git a/api/changelog.d/api-image-trivy-0-75-0.security.md b/api/changelog.d/api-image-trivy-0-75-0.security.md
deleted file mode 100644
index 4d5759d29c..0000000000
--- a/api/changelog.d/api-image-trivy-0-75-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the API container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy
diff --git a/api/changelog.d/api-image-trivy-0-76-0.security.md b/api/changelog.d/api-image-trivy-0-76-0.security.md
new file mode 100644
index 0000000000..59473ad497
--- /dev/null
+++ b/api/changelog.d/api-image-trivy-0-76-0.security.md
@@ -0,0 +1 @@
+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
diff --git a/api/changelog.d/django-5-2-17.security.md b/api/changelog.d/django-5-2-17.security.md
deleted file mode 100644
index ec1e6a7450..0000000000
--- a/api/changelog.d/django-5-2-17.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`django` from 5.1.15 to 5.2.17, patching CVE-2026-15307
diff --git a/api/changelog.d/dulwich-1-2-9.security.md b/api/changelog.d/dulwich-1-2-9.security.md
deleted file mode 100644
index 8f1dd24253..0000000000
--- a/api/changelog.d/dulwich-1-2-9.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx
diff --git a/api/changelog.d/kubeconfig-inline-credentials.security.md b/api/changelog.d/kubeconfig-inline-credentials.security.md
deleted file mode 100644
index 8d6115d4c3..0000000000
--- a/api/changelog.d/kubeconfig-inline-credentials.security.md
+++ /dev/null
@@ -1 +0,0 @@
-Kubernetes kubeconfig validation accepts only inline credentials and known cluster fields, rejecting file-path fields (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), command-based authentication (`exec`, `cmd-path`), `auth-provider` directives, proxy URLs and unknown user or cluster keys; stored kubeconfigs are re-validated before each scan and connection test
diff --git a/api/changelog.d/oci-home-region-bootstrap.fixed.md b/api/changelog.d/oci-home-region-bootstrap.fixed.md
deleted file mode 100644
index 0d28492c2b..0000000000
--- a/api/changelog.d/oci-home-region-bootstrap.fixed.md
+++ /dev/null
@@ -1 +0,0 @@
-OCI provider secrets keep the region as home region for credential validation and scans, instead of always using us-ashburn-1
diff --git a/api/changelog.d/pdf-report-account-id-overflow.fixed.md b/api/changelog.d/pdf-report-account-id-overflow.fixed.md
new file mode 100644
index 0000000000..d97d9bc41d
--- /dev/null
+++ b/api/changelog.d/pdf-report-account-id-overflow.fixed.md
@@ -0,0 +1 @@
+Compliance report PDF cover table wraps every value, so long account IDs such as OCI tenancy OCIDs no longer overflow their cell
diff --git a/api/changelog.d/pyjwt-2-14-0.security.md b/api/changelog.d/pyjwt-2-14-0.security.md
deleted file mode 100644
index fab78f2937..0000000000
--- a/api/changelog.d/pyjwt-2-14-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9
diff --git a/api/changelog.d/pymongo-4-18-2.security.md b/api/changelog.d/pymongo-4-18-2.security.md
deleted file mode 100644
index b621313d25..0000000000
--- a/api/changelog.d/pymongo-4-18-2.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749
diff --git a/api/changelog.d/sentry-no-personal-data.security.md b/api/changelog.d/sentry-no-personal-data.security.md
deleted file mode 100644
index b0c7d6d5fe..0000000000
--- a/api/changelog.d/sentry-no-personal-data.security.md
+++ /dev/null
@@ -1 +0,0 @@
-Sentry error events no longer include user identity, IP addresses, cookies, headers or request bodies, which could contain personal data or provider credentials
diff --git a/api/changelog.d/urllib3-2-8-0.security.md b/api/changelog.d/urllib3-2-8-0.security.md
deleted file mode 100644
index 4645bb5942..0000000000
--- a/api/changelog.d/urllib3-2-8-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689
diff --git a/api/changelog.d/user-detail-cross-tenant-relations.security.md b/api/changelog.d/user-detail-cross-tenant-relations.security.md
deleted file mode 100644
index 2ada35d1ea..0000000000
--- a/api/changelog.d/user-detail-cross-tenant-relations.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`GET /api/v1/users/{id}` no longer exposes another user's memberships, roles or tenant names from tenants other than the one in the request token
diff --git a/api/pyproject.toml b/api/pyproject.toml
index e20a4f3993..267bfd3d60 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -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.0"
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
# target-version tracks this project's lowest supported Python.
diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml
index 220afcb7d8..8c2bb7041d 100644
--- a/api/src/backend/api/specs/v1.yaml
+++ b/api/src/backend/api/specs/v1.yaml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
- version: 1.46.0
+ version: 1.47.0
description: |-
Prowler API specification.
diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py
index 664fcd1c4a..240514420e 100644
--- a/api/src/backend/config/django/base.py
+++ b/api/src/backend/config/django/base.py
@@ -130,6 +130,8 @@ REST_FRAMEWORK = {
SPECTACULAR_SETTINGS = {
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
+ # redoc@latest is 3.x, whose standalone bundle fails as a classic script
+ "REDOC_DIST": "https://cdn.jsdelivr.net/npm/redoc@2.5.1",
"PREPROCESSING_HOOKS": [
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
],
diff --git a/api/src/backend/config/guniconf.py b/api/src/backend/config/guniconf.py
index 8b17ad669d..5880d9f45c 100644
--- a/api/src/backend/config/guniconf.py
+++ b/api/src/backend/config/guniconf.py
@@ -53,6 +53,10 @@ preload_app = not DEBUG
# that may take longer, such as complex API operations.
timeout = env.int("GUNICORN_TIMEOUT", default=120)
+# Recycle workers after N requests (+ jitter) to cap memory growth. GUNICORN_MAX_REQUESTS=0 disables.
+max_requests = env.int("GUNICORN_MAX_REQUESTS", default=10000)
+max_requests_jitter = env.int("GUNICORN_MAX_REQUESTS_JITTER", default=1000)
+
# Logging
logconfig_dict = DJANGO_LOGGERS
gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)
diff --git a/api/src/backend/tasks/jobs/reports/cis.py b/api/src/backend/tasks/jobs/reports/cis.py
index 8a1cfb6eba..420c46252e 100644
--- a/api/src/backend/tasks/jobs/reports/cis.py
+++ b/api/src/backend/tasks/jobs/reports/cis.py
@@ -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(
diff --git a/api/src/backend/tasks/jobs/reports/ens.py b/api/src/backend/tasks/jobs/reports/ens.py
index 44c874bfc1..a940e95416 100644
--- a/api/src/backend/tasks/jobs/reports/ens.py
+++ b/api/src/backend/tasks/jobs/reports/ens.py
@@ -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(
diff --git a/api/src/backend/tasks/jobs/reports/nis2.py b/api/src/backend/tasks/jobs/reports/nis2.py
index ed936f9571..1775871f42 100644
--- a/api/src/backend/tasks/jobs/reports/nis2.py
+++ b/api/src/backend/tasks/jobs/reports/nis2.py
@@ -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(
diff --git a/api/src/backend/tasks/tests/test_reports_cis.py b/api/src/backend/tasks/tests/test_reports_cis.py
index b57780b8a4..530e58133d 100644
--- a/api/src/backend/tasks/tests/test_reports_cis.py
+++ b/api/src/backend/tasks/tests/test_reports_cis.py
@@ -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
diff --git a/api/src/backend/tasks/tests/test_reports_ens.py b/api/src/backend/tasks/tests/test_reports_ens.py
index 220c3e1427..ecb938bd38 100644
--- a/api/src/backend/tasks/tests/test_reports_ens.py
+++ b/api/src/backend/tasks/tests/test_reports_ens.py
@@ -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
):
diff --git a/api/src/backend/tasks/tests/test_reports_nis2.py b/api/src/backend/tasks/tests/test_reports_nis2.py
index ea6e06477d..ce630648f8 100644
--- a/api/src/backend/tasks/tests/test_reports_nis2.py
+++ b/api/src/backend/tasks/tests/test_reports_nis2.py
@@ -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
diff --git a/api/uv.lock b/api/uv.lock
index 58a1fffa90..afb8b9b2ca 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -4203,7 +4203,7 @@ wheels = [
[package.optional-dependencies]
broker = [
- { name = "pymsalruntime" },
+ { name = "pymsalruntime", marker = "sys_platform == 'win32'" },
]
[[package]]
@@ -4939,7 +4939,7 @@ dependencies = [
[[package]]
name = "prowler-api"
-version = "1.46.0"
+version = "1.47.0"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
index 78a2c12805..cd20063106 100644
--- a/docs/changelog.mdx
+++ b/docs/changelog.mdx
@@ -4,6 +4,68 @@ description: "Track new features, improvements, provider updates, and bug fixes
rss: true
---
+
+ ### 🤖 Azure — AI Services Coverage
+
+ A new Azure AI Services service audits the Azure AI and Azure OpenAI accounts of each subscription, with nine checks across network exposure, authentication, encryption, identity, logging and content safety, all contributed by @rajeshnadiminti. Thank you!
+
+ - `aiservices_account_public_network_access_disabled`, `aiservices_account_network_acls_default_action_deny`, `aiservices_account_uses_private_endpoint` and `aiservices_account_outbound_network_restricted` cover how the account can be reached and what it can reach.
+ - `aiservices_account_local_auth_disabled` and `aiservices_account_managed_identity_enabled` cover key-based access and the account's own identity.
+ - `aiservices_account_encrypted_with_cmk` and `aiservices_account_diagnostic_logging_enabled` cover customer-managed key encryption and diagnostic logs.
+ - `aiservices_deployment_content_filter_prompt_shield_enabled` verifies that model deployments have Prompt Shields enabled in their content filter.
+
+ ### 🪪 Microsoft 365 — New Entra ID Checks
+
+ Five new Entra ID checks, all contributed by @kinomakino. Thank you!
+
+ - `entra_cross_tenant_access_default_outbound_restricted` verifies that the default cross-tenant access policy does not let every user reach every external application through B2B collaboration or B2B direct connect.
+ - `entra_policy_admin_self_service_password_reset_disabled` verifies that Self-Service Password Reset is disabled for administrators.
+ - `entra_policy_msol_powershell_blocked` verifies that the tenant authorization policy blocks the legacy MSOnline PowerShell module.
+ - `entra_app_registration_redirect_uris_secure` flags app registrations with HTTP, wildcard, malformed or `azurewebsites.net` redirect URIs.
+ - `entra_dynamic_group_member_of_rule_not_used` flags dynamic groups whose membership rule uses the `memberOf` operator, which Microsoft retires on November 3, 2026.
+
+ Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
+
+ ### ☁️ OCI — Connect Tenancies Outside us-ashburn-1
+
+ OCI API key credentials now take a home region, and the credentials form in Prowler App requires it. Identity calls that validate the credentials and discover the subscribed regions used to go to `us-ashburn-1` regardless of the tenancy, so a tenancy not subscribed to that region could not connect. The home region is only used for that bootstrap; scans still audit every subscribed region.
+
+ Read more in the [Getting Started with OCI documentation](https://docs.prowler.com/user-guide/providers/oci/getting-started-oci).
+
+ ### 🛠️ Improvements and Fixes
+
+ - **AWS:** Organizations metadata is fetched from the partition of the audited account, using the region that answered the STS bootstrap, instead of the commercial `us-east-1` endpoint, which timed out in GovCloud.
+ - API Gunicorn workers are recycled after 10,000 requests, plus up to 1,000 of jitter, to cap memory growth. Both values are configurable with `GUNICORN_MAX_REQUESTS` and `GUNICORN_MAX_REQUESTS_JITTER`.
+ - The first-login redirect to the Add Provider wizard is retried on the next load when the navigation was cut short, instead of being written off after a single attempt.
+ - The AWS connect step offers to invite a teammate when the user cannot access the account credentials.
+ - **Prowler Cloud:** the Slack channel picker reads the channels the Prowler app is in before the public ones, so invited private channels show up in large workspaces, and the partial list message tells the user to invite the app to a missing channel.
+
+ ### 🔐 Security Updates
+
+ - Kubernetes kubeconfigs accept only inline credentials and known cluster fields. File-path fields, command-based authentication, `auth-provider` directives, proxy URLs and unknown keys are rejected by the API and the credentials form, and stored kubeconfigs are re-validated before each scan and connection test.
+ - IaC repository URLs that resolve to loopback, private or otherwise non-public hosts are rejected before the connection test and the scan clone, including every destination a redirect points to.
+ - CSV outputs prefix cells starting with `=`, `+`, `-`, `@`, tab or carriage return with a single quote so spreadsheet applications treat them as text, and the HTML report escapes the compliance column.
+ - `GET /api/v1/users/{id}` no longer exposes another user's memberships, roles or tenant names from tenants other than the one in the request token.
+ - API error reports sent to Sentry no longer include user identity, IP addresses, cookies, headers or request bodies.
+ - PowerShell upgraded to 7.5.12 in the SDK and API container images, bundling .NET runtime 9.0.21.
+ - Python base images upgraded to 3.12.15 in the SDK and API and to 3.13.16 in the MCP Server, and Trivy to 0.75.0, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy.
+ - `django` upgraded to 5.2.17 in the API, patching CVE-2026-15307.
+ - `pyjwt` upgraded to 2.14.0 in the SDK, the API and the MCP Server, patching GHSA-ffc3-869f-jxw9.
+ - `pymongo` upgraded to 4.18.2 and `urllib3` to 2.8.0 in the SDK and the API, patching CVE-2026-96748, CVE-2026-96749, CVE-2026-97687 and CVE-2026-97689.
+ - `dulwich` upgraded to 1.2.9 in the SDK and the API, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx.
+ - `next` upgraded to 16.3.8 in the UI, patching a remote code execution in `next/og` (GHSA-vcvr-r3jv-pc5j) and a server-side request forgery in Image Optimization (GHSA-cjq9-62q9-8jv4).
+ - `sharp` upgraded to 0.35.5 and `proxy-addr` to 2.0.8 in the UI, patching GHSA-wq5f-xc86-pv6w and GHSA-jqcg-44mw-7w3h.
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @rajeshnadiminti: Azure AI Services service and its checks ([#12925](https://github.com/prowler-cloud/prowler/pull/12925), [#12926](https://github.com/prowler-cloud/prowler/pull/12926), [#12997](https://github.com/prowler-cloud/prowler/pull/12997))
+ - @kinomakino: Microsoft 365 `entra_cross_tenant_access_default_outbound_restricted` ([#12991](https://github.com/prowler-cloud/prowler/pull/12991)), `entra_policy_admin_self_service_password_reset_disabled` ([#12990](https://github.com/prowler-cloud/prowler/pull/12990)), `entra_policy_msol_powershell_blocked` ([#13004](https://github.com/prowler-cloud/prowler/pull/13004)), `entra_app_registration_redirect_uris_secure` ([#12993](https://github.com/prowler-cloud/prowler/pull/12993)) and `entra_dynamic_group_member_of_rule_not_used` ([#13003](https://github.com/prowler-cloud/prowler/pull/13003)) checks
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.45.0) for the complete list of changes.
+
+
### 🔁 Findings — Re-check a Resource with a Partial Scan
diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx
index a0b07fc982..8cc060bbd5 100644
--- a/docs/getting-started/installation/prowler-app.mdx
+++ b/docs/getting-started/installation/prowler-app.mdx
@@ -129,8 +129,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"
```
diff --git a/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx b/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
index d926c9ea9b..9c972faad7 100644
--- a/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
+++ b/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
@@ -3,54 +3,337 @@ title: 'Subscribe to Prowler Cloud via AWS Marketplace'
description: 'Subscribe to Prowler Cloud through the AWS Marketplace listing, set up your account, and manage billing for cloud security scanning directly from AWS Billing.'
---
-This section contains the instructions to subscribe to **Prowler Cloud** through the **AWS Marketplace**.
+This page explains how to subscribe to **Prowler Cloud** through the **AWS Marketplace**, how to activate the subscription in Prowler Cloud, how billing works, and how to fix the errors you may hit on the way.
-## How to Subscribe
+## Plans
-To get to the **Prowler Cloud** product listing in the AWS Marketplace, and click the `View purchase options` button:
+Prowler Cloud has two listings in the AWS Marketplace. Both give you the same product. They differ in how you commit and how you pay.
-1. Use this link to be taken directly to the [Prowler Cloud Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-6ochhig5kxpok):
+A **provider** is a cloud account you connect to Prowler Cloud: an AWS account, an Azure subscription, a GCP project, a Kubernetes cluster, and so on.
- 
+| | Monthly (pay as you go) | Annual (billed annually) |
+| --- | --- | --- |
+| Listing | [Prowler Cloud $99/provider per month (Pay as You Go)](https://aws.amazon.com/marketplace/pp/prodview-nmbvelz746oyk) | [Prowler Cloud $79/provider per month (Billed Annually)](https://aws.amazon.com/marketplace/pp/prodview-wwehsmzui25ri) |
+| Billing | Monthly, per connected provider | Once, at checkout, for the full 12 months |
+| Commitment | None. Cancel anytime. | 12 months for the number of providers you choose during checkout on AWS. |
+| Resources beyond the included amount | Billed per resource | Billed per resource, at the annual rate |
+| Providers beyond your commitment | Not applicable | Billed monthly at the pay-as-you-go rate |
-2. Then, scroll down to the "Purchase Details" section and click the `Subscribe` button:
+For prices, included resources and per-resource rates, see the [Prowler Cloud pricing page](https://prowler.com/pricing/).
- 
+## Before You Subscribe
-## Set Up Your Account
+- **One AWS account pays for the whole organization.** Subscribe from a single AWS account. You then connect as many providers as you need, from any cloud, inside Prowler Cloud. You do not subscribe again for each AWS account you scan.
+- **Decide which Prowler Cloud organization will pay.** Activation links the AWS purchase to the organization you are signed in to. If you belong to several organizations, switch to the right one before you activate. See [Activate the Subscription in Prowler Cloud](#activate-the-subscription-in-prowler-cloud).
+- **Annual plan: decide the provider count first.** AWS asks for it during checkout, and AWS charges the full year at that moment. Providers you add beyond that count are billed monthly at the pay-as-you-go rate. AWS may not let you cancel an annual contract yourself from the console. See [Cancel the Subscription](#cancel-the-subscription) before you commit.
+- **Cancel any card-billed subscription first.** An organization already billed by credit card cannot switch to AWS Marketplace billing until that subscription is cancelled. This prevents double charges.
-After you have subscribed to the **Prowler Cloud** product, you will need to set up your **Prowler Cloud** account:
+## Subscribe on AWS Marketplace
-1. Click the `Set up your account` button:
+
+
+ Sign in to the AWS account that will pay, open the listing for the plan you want, and click **View purchase options**.
- 
+
+
+ Open [Prowler Cloud $99/provider per month (Pay as You Go)](https://aws.amazon.com/marketplace/pp/prodview-nmbvelz746oyk).
-2. You will be redirected to **Prowler Cloud Sign In** page. You can sign in with an existing account or sign up with a new account:
+
+
+
+
+
+ Open [Prowler Cloud $79/provider per month (Billed Annually)](https://aws.amazon.com/marketplace/pp/prodview-wwehsmzui25ri).
- 
+
+
+
+
+
+
-3. Once you have successfully authenticated, you should be automatically redirected to the **Prowler Cloud** [Billing](https://cloud.prowler.com/billing) page where you should now see that your account has the `AWS Marketplace Subscription` badge.
+
+
+
+ Scroll to **Purchase details** and click **Subscribe**. There is no upfront charge. AWS bills your usage at the end of each month.
- 
+
+
+
+
+
+ Under **Pricing details**, select the **providers** dimension and enter the **Number of units**. One unit is one provider for the 12-month term. Review the **Contract total**, then click **Subscribe** at the bottom of the page. AWS charges the full year now.
-If you have any issues signing up, please contact us at support@prowler.com.
+
+
+
+ **Auto-renewal** is on by default. Leave it on to have AWS renew the contract for another 12 months at the end date, or choose **Do not automatically renew this contract**. You can change this from **Manage subscriptions** any time before the renewal date.
+
+
+
-## Billing
+
+ AWS confirms the purchase and shows a **Set up your account** button. Click it. AWS sends your browser to Prowler Cloud with a setup link.
-You will be charged monthly based on resources scanned and monitored depending on usage in **Prowler Cloud**. For more information on billing, please see the [Prowler Cloud Pricing FAQ](https://prowler.com/pricing/).
+
+
+
-**Note:** Your **Prowler Cloud** bills can be seen at [AWS Billing](https://us-east-1.console.aws.amazon.com/billing/home#/bills).
+
+ The setup link expires quickly: AWS documents roughly one hour, and never more than four. If the link expires, you close the tab, or need to start over, return to [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) in the AWS Marketplace console, open **Prowler Cloud**, and click **Set up your account** again to get a fresh link.
+
+
+
-## Subscription
+## Activate the Subscription in Prowler Cloud
-If you subscribe to Prowler Cloud through the AWS Marketplace it is not necessary to subscribe from different AWS accounts to use Prowler Cloud for those accounts.
+Nothing is linked until you confirm it in Prowler Cloud. The purchase is safe on the AWS side while you complete these steps.
-In Prowler Cloud you only need to subscribe from one of your AWS accounts through the AWS Marketplace and add multiple provider accounts once you are in the Prowler Cloud console. We will send usage metrics to the AWS Marketplace regardless of the number of accounts you add in our platform, so the AWS Marketplace will bill you based on those usage metrics.
+
+
+ Prowler Cloud shows the sign-in page with a banner that reads "To finish setting up your AWS Marketplace subscription, sign in or register if you don't have an account yet."
+
+
+
+
+
+ If you do not have an account yet, click **Sign up**. The purchase carries over once you sign in with the new account.
+
+
+
+
+
+
+
+ The billing page shows a card titled **Activate your AWS Marketplace subscription**. It names the organization the purchase will be linked to.
+
+
+
+
+
+
+ Read the organization name before you click. Activation is a billing decision: charges for that organization go to the AWS account you subscribed with. If it is not the organization you want to pay for, switch organizations first, then come back to the billing page.
+
+
+ Click **Activate subscription**. Prowler Cloud links the purchase and activates the subscription. Two short status messages appear while it works: "Linking your AWS Marketplace purchase…" and "Activating your subscription…".
+
+
+
+
+
+ **Continue to billing without activating** discards the setup link and leaves the organization unchanged. Use it only if you landed in the wrong organization. To activate later, get a fresh link from the AWS Marketplace console as described above.
+
+
+
+ The billing page now states that you subscribed through AWS Marketplace, shows the AWS account that pays, and links to your subscription in the AWS Marketplace console.
+
+
+
+
+
+ There is no payment method, invoice history, or cancel button on this page. AWS handles all of that.
+
+
+
+If something goes wrong during activation, see [Activation errors](#activation-errors).
+
+## After Activation
+
+- Add providers from the **Providers** page in Prowler Cloud. Every provider you add is covered by the same AWS subscription, whatever cloud it belongs to.
+- All charges come from AWS. Find them under **Amazon Web Services, Inc. - Marketplace** in [AWS Billing](https://console.aws.amazon.com/billing/home#/bills).
+
+
+
+
+
+### Monthly
+
+AWS bills you at the end of each month for the providers connected and the resources scanned in Prowler Cloud during that month.
+
+### Annual
+
+AWS charges the full 12-month subscription once, at checkout. The providers covered by your commitment cost nothing more during the year. Your monthly AWS bill only includes usage beyond the commitment: extra providers at the pay-as-you-go rate, and resources beyond the included amount at the annual plan's per-resource rate.
+
+If auto-renewal is on, AWS renews the contract for another 12 months at the end date and charges the new term the same way. Turn it off from **Manage subscriptions** before the renewal date if you do not want that.
+
+## Cancel the Subscription
+
+There is no cancel button in Prowler Cloud for AWS Marketplace subscriptions. How you cancel depends on the plan, following the [AWS Marketplace cancellation rules](https://docs.aws.amazon.com/marketplace/latest/buyerguide/cancel-subscription.html).
+
+### Monthly
+
+Cancel from the AWS Marketplace console at any time:
+
+1. Open [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) and open **Prowler Cloud**.
+2. Under **Active agreements**, select the agreement and click **View details**.
+3. Open the **Actions** menu and choose **Cancel subscription**.
+4. Type `confirm` and choose **Yes, cancel subscription**.
+
+
+
+
+
+Cancelling stops future charges. Usage accrued before the cancellation still appears on your next AWS bill.
+
+### Annual
+
+Open the agreement from [Manage subscriptions](https://console.aws.amazon.com/marketplace/subscriptions) first. If the **Actions** menu shows **Cancel subscription**, follow the monthly steps above. If it does not, AWS does not allow self-service cancellation for that contract:
+
+- **Within 48 hours of purchase**, contact AWS Customer Service. AWS can cancel the contract and refund the full amount, but only if you ask for the refund.
+- **After 48 hours**, contact [support@prowler.com](mailto:support@prowler.com). Prowler starts the cancellation request through AWS Marketplace and you approve it in the AWS Marketplace console. A refund is not automatic. Ask for it when you request the cancellation.
+
+See [Refunds and cancellations in AWS Marketplace](https://docs.aws.amazon.com/marketplace/latest/buyerguide/buyer-refunds.html) for the AWS side of the process.
+
+### What happens after cancellation
+
+Once AWS ends the subscription, Prowler Cloud pauses scans for the organization. Providers, findings and scan history stay readable. A **Subscription required** card in the sidebar, labeled **Subscription cancelled**, explains it: "Scans are paused and your assessment data remains available. Subscribe again to continue scanning." The billing page shows the plan selection page.
+
+
+
+
+
+To subscribe again from the same AWS account into the same Prowler Cloud organization, subscribe from the listing and activate as before.
+
+
+Deleting your Prowler Cloud organization does not cancel the AWS subscription. AWS keeps charging the agreement until you cancel it as described above.
+
## Troubleshooting
-### SEPA Payment Method Issues
+### Activation errors
+
+Each error appears on the Prowler Cloud billing page with the title below. Your AWS purchase is never lost by any of them. **Continue to billing** closes the message without activating.
+
+
+
+ AWS no longer accepts the setup link, usually because it timed out. AWS documents roughly one hour of validity, and never more than four.
+
+ Open **Manage subscriptions** in the AWS Marketplace console, open **Prowler Cloud**, and click **Set up your account** to get a fresh link.
+
+
+
+
+
+
+
+ The purchase went through on AWS, but this listing is not enabled for activation in Prowler Cloud. You see this when you subscribed to an earlier Prowler Cloud listing instead of one of the [two current plans](#plans).
+
+ Contact [support@prowler.com](mailto:support@prowler.com) with the AWS account you subscribed with. Click **Try again** once support confirms.
+
+
+
+
+
+
+
+ The purchase cannot be linked to the organization you are signed in to. The message shows the reason reported for this purchase:
+
+ - **"This AWS Marketplace subscription is no longer active."** The subscription was cancelled on AWS. Subscribe again from the listing. If you see this seconds after a new purchase, AWS may still be registering it: wait a minute, go back to **Manage subscriptions**, and click **Set up your account** again.
+ - **"This AWS Marketplace purchase is already linked to another Prowler account."** AWS identifies a purchase by AWS account and product, so one AWS account backs one Prowler Cloud organization per plan. Sign in to the organization that already holds it, or subscribe from a different AWS account.
+ - **"This account is already linked to another active AWS Marketplace purchase."** This organization already has an active AWS Marketplace subscription. Cancel that subscription in the AWS Marketplace console before activating a new one.
+
+ If none of these applies, contact [support@prowler.com](mailto:support@prowler.com).
+
+
+
+
+
+
+
+ Another request is linking the same purchase, usually a second click or a second tab. Wait a moment and click **Try again**.
+
+
+
+
+
+
+
+ This organization is billed by credit card. Cancel that subscription first, so you are never charged twice. Then open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** again.
+
+
+
+
+
+
+
+ AWS Marketplace billing for this AWS account and plan is already set up in a different Prowler Cloud organization. This also happens after you delete the organization that held the subscription and subscribe again from a new one.
+
+ Sign in to the organization that holds the billing and restart the setup from **Manage subscriptions** in the AWS Marketplace console. If that organization no longer exists, contact [support@prowler.com](mailto:support@prowler.com) so they can release the billing link.
+
+
+
+ Prowler Cloud received no purchase to activate. Open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** to start again.
+
+
+
+
+
+
+
+ A temporary problem talking to AWS. Click **Try again**. If it keeps failing, contact [support@prowler.com](mailto:support@prowler.com) and include the detail text shown under the message.
+
+
+
+
+
+
+
+ The purchase was linked but the activation did not complete. Click **Try again**. If it keeps failing, contact [support@prowler.com](mailto:support@prowler.com) and include the detail text shown under the message.
+
+
+
+
+
+
+
+ Your Prowler Cloud session expired before the activation finished. Click **Sign in again** and pick up where you left off. The setup link stays valid during its expiry window.
+
+
+
+
+
+
+
+ The setup link arrived damaged. Open **Manage subscriptions** in the AWS Marketplace console and click **Set up your account** to get a fresh one.
+
+
+
+
+
+
+
+### Errors before the sign-in page
+
+If the redirect from AWS itself fails, Prowler Cloud shows a plain page instead of the app. Each page links back to Prowler Cloud billing and to your AWS Marketplace subscriptions.
+
+
+
+ The setup page only accepts the redirect from AWS Marketplace. You reach it by opening the setup URL directly or from a bookmark. Start again from **Set up your account** in the AWS Marketplace console.
+
+
+
+
+
+
+
+ The redirect arrived without a valid setup token. Start again from **Set up your account** in the AWS Marketplace console.
+
+
+
+
+
+
+
+ Prowler Cloud could not process the redirect. Your purchase is safe. Wait a moment, then start again from **Set up your account** in the AWS Marketplace console. Contact [support@prowler.com](mailto:support@prowler.com) if it happens again.
+
+
+
+
+
+
+
+### SEPA payment method issues
If AWS Marketplace notifies that payment failed due to an issue with the payment method, this typically occurs when a SEPA bank account is set as the default payment method. AWS Marketplace does not support SEPA bank accounts for product subscriptions, even when the account includes valid alternative payment methods. This is because AWS Marketplace invoices are issued by AWS Inc., a US entity. SEPA accounts do not recognize these invoices as valid, causing subscription failures.
@@ -74,10 +357,14 @@ The AWS Marketplace team acknowledges this limitation: "We apologize for these a
-### Credit and Debit Card Storage Restrictions (AISPL Customers in India)
+### Credit and debit card storage restrictions (AISPL customers in India)
AWS Marketplace no longer supports payments using credit or debit cards stored on file for Amazon Internet Services Private Limited (AISPL) customers. This restriction stems from Reserve Bank of India (RBI) regulations regarding payment aggregators, which prohibit the storage of card data. As explained in [this AWS blog post](https://aws.amazon.com/blogs/awsmarketplace/restriction-on-credit-and-debit-card-purchases-for-aispl-customers-using-aws-marketplace/):
> AWS Marketplace can no longer support payments using credit or debit cards stored on file. [The Reserve Bank of India (RBI) has issued a notice regarding regulation of payment aggregators](https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=11822&Mode=0), which restricts the storage of card data. If you are currently using credit or debit card as your default payment instrument, your ability to use AWS Marketplace products will be restricted. However, you can switch your default payment instrument to Pay By Invoice to avoid disruption or restore your original experience.
To maintain uninterrupted access to AWS Marketplace products, change the default payment instrument from stored card data to Pay By Invoice billing.
+
+## Support
+
+If you get stuck at any step, contact [support@prowler.com](mailto:support@prowler.com) and include the AWS account you subscribed with and the exact error title you see.
diff --git a/docs/images/aws-marketplace/activate-confirm.png b/docs/images/aws-marketplace/activate-confirm.png
new file mode 100644
index 0000000000..5f0b4c7659
Binary files /dev/null and b/docs/images/aws-marketplace/activate-confirm.png differ
diff --git a/docs/images/aws-marketplace/activate-working.png b/docs/images/aws-marketplace/activate-working.png
new file mode 100644
index 0000000000..91ef40d029
Binary files /dev/null and b/docs/images/aws-marketplace/activate-working.png differ
diff --git a/docs/images/aws-marketplace/aws-manage-subscriptions.png b/docs/images/aws-marketplace/aws-manage-subscriptions.png
new file mode 100644
index 0000000000..0d02aad4a4
Binary files /dev/null and b/docs/images/aws-marketplace/aws-manage-subscriptions.png differ
diff --git a/docs/images/aws-marketplace/billing-cancelled.png b/docs/images/aws-marketplace/billing-cancelled.png
new file mode 100644
index 0000000000..01681eb706
Binary files /dev/null and b/docs/images/aws-marketplace/billing-cancelled.png differ
diff --git a/docs/images/aws-marketplace/billing-subscribed.png b/docs/images/aws-marketplace/billing-subscribed.png
new file mode 100644
index 0000000000..339adaf8d0
Binary files /dev/null and b/docs/images/aws-marketplace/billing-subscribed.png differ
diff --git a/docs/images/aws-marketplace/discover-product.png b/docs/images/aws-marketplace/discover-product.png
deleted file mode 100644
index b78f0c6fc4..0000000000
Binary files a/docs/images/aws-marketplace/discover-product.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/error-agreement-in-use.png b/docs/images/aws-marketplace/error-agreement-in-use.png
new file mode 100644
index 0000000000..cd4387cba1
Binary files /dev/null and b/docs/images/aws-marketplace/error-agreement-in-use.png differ
diff --git a/docs/images/aws-marketplace/error-billing-provider-conflict.png b/docs/images/aws-marketplace/error-billing-provider-conflict.png
new file mode 100644
index 0000000000..1bfb535a02
Binary files /dev/null and b/docs/images/aws-marketplace/error-billing-provider-conflict.png differ
diff --git a/docs/images/aws-marketplace/error-invalid-input.png b/docs/images/aws-marketplace/error-invalid-input.png
new file mode 100644
index 0000000000..83b7d83ac8
Binary files /dev/null and b/docs/images/aws-marketplace/error-invalid-input.png differ
diff --git a/docs/images/aws-marketplace/error-link-in-progress.png b/docs/images/aws-marketplace/error-link-in-progress.png
new file mode 100644
index 0000000000..3015e6752e
Binary files /dev/null and b/docs/images/aws-marketplace/error-link-in-progress.png differ
diff --git a/docs/images/aws-marketplace/error-no-agreement.png b/docs/images/aws-marketplace/error-no-agreement.png
new file mode 100644
index 0000000000..a792829d12
Binary files /dev/null and b/docs/images/aws-marketplace/error-no-agreement.png differ
diff --git a/docs/images/aws-marketplace/error-product-not-supported.png b/docs/images/aws-marketplace/error-product-not-supported.png
new file mode 100644
index 0000000000..97d1d0aaf3
Binary files /dev/null and b/docs/images/aws-marketplace/error-product-not-supported.png differ
diff --git a/docs/images/aws-marketplace/error-resolve-failed.png b/docs/images/aws-marketplace/error-resolve-failed.png
new file mode 100644
index 0000000000..2a029bf188
Binary files /dev/null and b/docs/images/aws-marketplace/error-resolve-failed.png differ
diff --git a/docs/images/aws-marketplace/error-subscribe-failed.png b/docs/images/aws-marketplace/error-subscribe-failed.png
new file mode 100644
index 0000000000..de7c840377
Binary files /dev/null and b/docs/images/aws-marketplace/error-subscribe-failed.png differ
diff --git a/docs/images/aws-marketplace/error-token-expired.png b/docs/images/aws-marketplace/error-token-expired.png
new file mode 100644
index 0000000000..3f7ffc2a8a
Binary files /dev/null and b/docs/images/aws-marketplace/error-token-expired.png differ
diff --git a/docs/images/aws-marketplace/error-unauthorized.png b/docs/images/aws-marketplace/error-unauthorized.png
new file mode 100644
index 0000000000..084c5eff21
Binary files /dev/null and b/docs/images/aws-marketplace/error-unauthorized.png differ
diff --git a/docs/images/aws-marketplace/fulfillment-error-generic.png b/docs/images/aws-marketplace/fulfillment-error-generic.png
new file mode 100644
index 0000000000..dafa3e7b86
Binary files /dev/null and b/docs/images/aws-marketplace/fulfillment-error-generic.png differ
diff --git a/docs/images/aws-marketplace/fulfillment-error-read.png b/docs/images/aws-marketplace/fulfillment-error-read.png
new file mode 100644
index 0000000000..ceb3036a9d
Binary files /dev/null and b/docs/images/aws-marketplace/fulfillment-error-read.png differ
diff --git a/docs/images/aws-marketplace/fulfillment-error-verify.png b/docs/images/aws-marketplace/fulfillment-error-verify.png
new file mode 100644
index 0000000000..ae11eca36e
Binary files /dev/null and b/docs/images/aws-marketplace/fulfillment-error-verify.png differ
diff --git a/docs/images/aws-marketplace/listing-annual.png b/docs/images/aws-marketplace/listing-annual.png
new file mode 100644
index 0000000000..e9609794e9
Binary files /dev/null and b/docs/images/aws-marketplace/listing-annual.png differ
diff --git a/docs/images/aws-marketplace/listing-monthly.png b/docs/images/aws-marketplace/listing-monthly.png
new file mode 100644
index 0000000000..944cae4731
Binary files /dev/null and b/docs/images/aws-marketplace/listing-monthly.png differ
diff --git a/docs/images/aws-marketplace/marketplace-listing.png b/docs/images/aws-marketplace/marketplace-listing.png
deleted file mode 100644
index aae03ee438..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-listing.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/marketplace-message.png b/docs/images/aws-marketplace/marketplace-message.png
deleted file mode 100644
index 7b4581273c..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-message.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/marketplace-my-account.png b/docs/images/aws-marketplace/marketplace-my-account.png
deleted file mode 100644
index 85fc08f932..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-my-account.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/marketplace-sign-up.png b/docs/images/aws-marketplace/marketplace-sign-up.png
deleted file mode 100644
index 249bbb6c56..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-sign-up.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/marketplace-subscribe.png b/docs/images/aws-marketplace/marketplace-subscribe.png
deleted file mode 100644
index 033e7f156d..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-subscribe.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/marketplace-unsubscribe-error.png b/docs/images/aws-marketplace/marketplace-unsubscribe-error.png
deleted file mode 100644
index 97c81abe71..0000000000
Binary files a/docs/images/aws-marketplace/marketplace-unsubscribe-error.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/my-account.png b/docs/images/aws-marketplace/my-account.png
deleted file mode 100644
index 3476a7e5aa..0000000000
Binary files a/docs/images/aws-marketplace/my-account.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/purchase-annual.png b/docs/images/aws-marketplace/purchase-annual.png
new file mode 100644
index 0000000000..83a57a912a
Binary files /dev/null and b/docs/images/aws-marketplace/purchase-annual.png differ
diff --git a/docs/images/aws-marketplace/purchase-monthly.png b/docs/images/aws-marketplace/purchase-monthly.png
new file mode 100644
index 0000000000..488239d4bd
Binary files /dev/null and b/docs/images/aws-marketplace/purchase-monthly.png differ
diff --git a/docs/images/aws-marketplace/set-up-your-account.png b/docs/images/aws-marketplace/set-up-your-account.png
new file mode 100644
index 0000000000..4769dfd509
Binary files /dev/null and b/docs/images/aws-marketplace/set-up-your-account.png differ
diff --git a/docs/images/aws-marketplace/sign-in-marketplace.png b/docs/images/aws-marketplace/sign-in-marketplace.png
new file mode 100644
index 0000000000..dc2e350d97
Binary files /dev/null and b/docs/images/aws-marketplace/sign-in-marketplace.png differ
diff --git a/docs/images/aws-marketplace/sign-up-marketplace.png b/docs/images/aws-marketplace/sign-up-marketplace.png
new file mode 100644
index 0000000000..8fe2f45356
Binary files /dev/null and b/docs/images/aws-marketplace/sign-up-marketplace.png differ
diff --git a/docs/images/aws-marketplace/subscription-in-use.png b/docs/images/aws-marketplace/subscription-in-use.png
deleted file mode 100644
index 7797170f6b..0000000000
Binary files a/docs/images/aws-marketplace/subscription-in-use.png and /dev/null differ
diff --git a/docs/images/aws-marketplace/subscription-pending.png b/docs/images/aws-marketplace/subscription-pending.png
deleted file mode 100644
index bb608afd68..0000000000
Binary files a/docs/images/aws-marketplace/subscription-pending.png and /dev/null differ
diff --git a/docs/user-guide/providers/iac/authentication.mdx b/docs/user-guide/providers/iac/authentication.mdx
index bbc7409dc3..5ca95ef991 100644
--- a/docs/user-guide/providers/iac/authentication.mdx
+++ b/docs/user-guide/providers/iac/authentication.mdx
@@ -12,3 +12,28 @@ Prowler's Infrastructure as Code (IaC) provider enables you to scan local or rem
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
+
+### Private Repository Hosts
+
+**Prowler CLI scans a repository on an internal network with no extra configuration.** On the CLI the operator supplies the repository URL themselves, so there is nothing to defend against and the check below does not apply.
+
+**Prowler App rejects a repository URL that resolves to a non-public address**, as an SSRF defense. There the URL arrives from a tenant and the worker must not be used to reach the internal network it runs in. The check covers both the connection test and the scan clone. To scan a self-hosted GitLab, Gitea or Bitbucket 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 OpenStack providers. The Image provider has its own, `PROWLER_IMAGE_PROVIDER_ALLOWED_PRIVATE_NETWORKS`.
diff --git a/docs/user-guide/tutorials/prowler-app-slack-integration.mdx b/docs/user-guide/tutorials/prowler-app-slack-integration.mdx
index f9ae3f99b1..a75389f329 100644
--- a/docs/user-guide/tutorials/prowler-app-slack-integration.mdx
+++ b/docs/user-guide/tutorials/prowler-app-slack-integration.mdx
@@ -27,7 +27,7 @@ This guide covers the Slack integration in Prowler Cloud and Prowler Private Clo
When connected and configured:
1. A Slack workspace is approved once through Slack's app install flow, and Prowler stores the resulting credential encrypted.
-2. Prowler reads the channels it can post to: the workspace's public channels, plus the private channels the Prowler app has been invited to.
+2. Prowler reads the channels it can post to: first the channels the Prowler app is already in, public or private, then the rest of the workspace's public channels.
3. Several of those channels are selected and saved as the integration's authorized channels.
4. The connection check verifies the credential and every authorized channel, and posts a one-time confirmation message to each channel it has not confirmed yet.
5. Features that deliver to Slack, such as [Alerts](/user-guide/tutorials/prowler-alerts), choose their destinations from the confirmed channels.
@@ -98,7 +98,7 @@ Declining the consent screen creates nothing. Prowler reports that the workspace
Prowler posts to the channels authorized on the integration. Several channels can be authorized at once, and once the connection check has confirmed them they are the pool every consumer of the integration draws from: an [Alert](/user-guide/tutorials/prowler-alerts) picks its Slack destinations from the confirmed channels, never from the whole workspace.
-1. Open the **Destination channels** selection. It lists the workspace's public channels, plus the private channels the Prowler app has been invited to, each marked **Private**.
+1. Open the **Destination channels** selection. It lists the channels the Prowler app is already in, including the private channels it has been invited to, each marked **Private**, followed by the rest of the workspace's public channels.

@@ -109,7 +109,7 @@ Prowler validates the selection against Slack and derives each channel name itse
If the selection reports that no channels are available, the workspace exposes nothing Prowler can see. Create a public channel, or invite `@Prowler Cloud` to a private one, then click **Refresh channels**.
-A workspace can hold more channels than Prowler reads in one go. When that happens, the selection says so and lists what was read: every listed channel is usable, and a channel missing from a partial list is not necessarily one `@Prowler Cloud` has to be invited to. Only listed channels can be selected: **Refresh channels** repeats the same bounded read rather than reading further, and the selection's search filters what was already read, so neither surfaces a channel the read left out.
+A workspace can hold more public channels than Prowler reads in one go. When that happens, the selection says so and lists what was read: every listed channel is usable, and the channels `@Prowler Cloud` is already in are always among them, because they are read first. Only listed channels can be selected: **Refresh channels** repeats the same bounded read rather than reading further, and the selection's search filters what was already read, so neither surfaces a public channel the read left out. To make such a channel selectable, invite `@Prowler Cloud` to it and click **Refresh channels**.
Saving a new selection replaces the authorized set: channels left out of it stop being authorized, and channels added to it are authorized but not yet confirmed. Changing which channels are in the set also resets the integration's connection state, so the check runs again over the new set — reordering the same channels does not. Saving an empty selection leaves the integration with no authorized channels, and **Test connection** cannot be run again until at least one channel is authorized.
diff --git a/mcp_server/CHANGELOG.md b/mcp_server/CHANGELOG.md
index 07e92992fc..4f50280aeb 100644
--- a/mcp_server/CHANGELOG.md
+++ b/mcp_server/CHANGELOG.md
@@ -4,6 +4,15 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
+## [0.12.3] (Prowler v5.45.0)
+
+### 🔐 Security
+
+- `pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9 [(#12911)](https://github.com/prowler-cloud/prowler/pull/12911)
+- Python from 3.13.14 to 3.13.16 in the MCP container image, patching CVE-2026-19445 and CVE-2026-19553 [(#13009)](https://github.com/prowler-cloud/prowler/pull/13009)
+
+---
+
## [0.12.2] (Prowler v5.43.0)
### 🔐 Security
diff --git a/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md b/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md
deleted file mode 100644
index c926794e3e..0000000000
--- a/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md
+++ /dev/null
@@ -1 +0,0 @@
-Python from 3.13.14 to 3.13.16 in the MCP container image, patching CVE-2026-19445 and CVE-2026-19553
diff --git a/mcp_server/changelog.d/pyjwt-2-14-0.security.md b/mcp_server/changelog.d/pyjwt-2-14-0.security.md
deleted file mode 100644
index fab78f2937..0000000000
--- a/mcp_server/changelog.d/pyjwt-2-14-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index d424abdd01..5b43d32051 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -4,6 +4,38 @@ All notable changes to the **Prowler SDK** are documented in this file.
+## [5.45.0] (Prowler v5.45.0)
+
+### 🚀 Added
+
+- Azure AI Services service for Azure provider, including the `aiservices_account_public_network_access_disabled`, `aiservices_account_local_auth_disabled`, and `aiservices_account_encrypted_with_cmk` checks [(#12925)](https://github.com/prowler-cloud/prowler/pull/12925)
+- `aiservices_account_uses_private_endpoint`, `aiservices_account_outbound_network_restricted`, `aiservices_account_managed_identity_enabled`, and `aiservices_account_diagnostic_logging_enabled` checks for Azure provider [(#12926)](https://github.com/prowler-cloud/prowler/pull/12926)
+- `entra_policy_admin_self_service_password_reset_disabled` check for M365 provider, verifying that Self-Service Password Reset is disabled for administrators [(#12990)](https://github.com/prowler-cloud/prowler/pull/12990)
+- `entra_cross_tenant_access_default_outbound_restricted` check for M365 provider, verifying that default cross-tenant access does not let all users reach all external applications through B2B collaboration or B2B direct connect [(#12991)](https://github.com/prowler-cloud/prowler/pull/12991)
+- `entra_app_registration_redirect_uris_secure` check for M365 provider, flagging app registrations with HTTP, wildcard, malformed or `azurewebsites.net` redirect URIs [(#12993)](https://github.com/prowler-cloud/prowler/pull/12993)
+- `aiservices_account_network_acls_default_action_deny` and `aiservices_deployment_content_filter_prompt_shield_enabled` checks for Azure provider [(#12997)](https://github.com/prowler-cloud/prowler/pull/12997)
+- `entra_dynamic_group_member_of_rule_not_used` check for M365 provider, flagging dynamic groups whose membership rule uses the `memberOf` operator that Microsoft retires on November 3, 2026 [(#13003)](https://github.com/prowler-cloud/prowler/pull/13003)
+- `entra_policy_msol_powershell_blocked` check for M365 provider, verifying that the tenant authorization policy blocks the legacy MSOnline PowerShell module [(#13004)](https://github.com/prowler-cloud/prowler/pull/13004)
+
+### 🐞 Fixed
+
+- OCI API key credentials accept a home region to bootstrap identity calls, so tenancies not subscribed to us-ashburn-1 can connect [(#12865)](https://github.com/prowler-cloud/prowler/pull/12865)
+- AWS Organizations metadata is fetched from the partition of the audited account, using the region that answered the STS bootstrap, instead of the commercial `us-east-1` endpoint a session without a region resolves to, which timed out in GovCloud [(#12988)](https://github.com/prowler-cloud/prowler/pull/12988)
+
+### 🔐 Security
+
+- `pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9 [(#12911)](https://github.com/prowler-cloud/prowler/pull/12911)
+- `dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
+- `urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689 [(#12953)](https://github.com/prowler-cloud/prowler/pull/12953)
+- `pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749 [(#12955)](https://github.com/prowler-cloud/prowler/pull/12955)
+- IaC repository URLs resolving to loopback, private or otherwise non-public hosts rejected before both the connection test and the scan clone, including every destination a redirect points to, with raw errors no longer echoed to the caller or the logs [(#12960)](https://github.com/prowler-cloud/prowler/pull/12960)
+- HTML report compliance column is now escaped, so framework names and requirement IDs containing markup are shown as text instead of being rendered by the browser [(#12963)](https://github.com/prowler-cloud/prowler/pull/12963)
+- CSV outputs (findings, compliance and AWS quick inventory) prefix cells starting with `=`, `+`, `-`, `@`, tab or carriage return with a single quote so spreadsheet applications treat them as text instead of formulas; signed numbers such as `-1` are left untouched [(#12963)](https://github.com/prowler-cloud/prowler/pull/12963)
+- Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the SDK container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy [(#13009)](https://github.com/prowler-cloud/prowler/pull/13009)
+- PowerShell from 7.5.11 to 7.5.12 in the SDK container image, bundling .NET runtime 9.0.21 [(#13037)](https://github.com/prowler-cloud/prowler/pull/13037)
+
+---
+
## [5.44.0] (Prowler v5.44.0)
### 🚀 Added
diff --git a/prowler/__main__.py b/prowler/__main__.py
index b9de47c07b..669c1c81b9 100644
--- a/prowler/__main__.py
+++ b/prowler/__main__.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import os
import sys
import tempfile
from os import environ
@@ -55,6 +56,7 @@ from prowler.lib.check.custom_checks_metadata import (
from prowler.lib.check.models import CheckMetadata
from prowler.lib.cli.parser import ProwlerArgumentParser
from prowler.lib.logger import logger, set_logging_config
+from prowler.lib.network.ssrf import SKIP_OUTBOUND_CHECK_ENV
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.compliance.asd_essential_eight.asd_essential_eight_aws import (
ASDEssentialEightAWS,
@@ -236,6 +238,11 @@ def _send_ocsf_to_cloud(file_path: str) -> dict | None:
def prowler():
+ # On the CLI the operator supplies the target themselves, so the outbound host
+ # check defends nothing and would refuse their own private network. setdefault,
+ # so an operator who set the variable explicitly keeps their choice.
+ os.environ.setdefault(SKIP_OUTBOUND_CHECK_ENV, "true")
+
# Parse Arguments
# Refactor(CLI)
parser = ProwlerArgumentParser()
diff --git a/prowler/changelog.d/aws-organizations-partition-region.fixed.md b/prowler/changelog.d/aws-organizations-partition-region.fixed.md
deleted file mode 100644
index 580a69ec05..0000000000
--- a/prowler/changelog.d/aws-organizations-partition-region.fixed.md
+++ /dev/null
@@ -1 +0,0 @@
-AWS Organizations metadata is fetched from the partition of the audited account, using the region that answered the STS bootstrap, instead of the commercial `us-east-1` endpoint a session without a region resolves to, which timed out in GovCloud
diff --git a/prowler/changelog.d/dulwich-1-2-9.security.md b/prowler/changelog.d/dulwich-1-2-9.security.md
deleted file mode 100644
index 8f1dd24253..0000000000
--- a/prowler/changelog.d/dulwich-1-2-9.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx
diff --git a/prowler/changelog.d/gcp-vertexai-workbench-checks.added.md b/prowler/changelog.d/gcp-vertexai-workbench-checks.added.md
new file mode 100644
index 0000000000..99fb7a246c
--- /dev/null
+++ b/prowler/changelog.d/gcp-vertexai-workbench-checks.added.md
@@ -0,0 +1 @@
+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
diff --git a/prowler/changelog.d/oci-home-region-bootstrap.fixed.md b/prowler/changelog.d/oci-home-region-bootstrap.fixed.md
deleted file mode 100644
index de2997773d..0000000000
--- a/prowler/changelog.d/oci-home-region-bootstrap.fixed.md
+++ /dev/null
@@ -1 +0,0 @@
-OCI API key credentials accept a home region to bootstrap identity calls, so tenancies not subscribed to us-ashburn-1 can connect
diff --git a/prowler/changelog.d/pyjwt-2-14-0.security.md b/prowler/changelog.d/pyjwt-2-14-0.security.md
deleted file mode 100644
index fab78f2937..0000000000
--- a/prowler/changelog.d/pyjwt-2-14-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`pyjwt` from 2.13.0 to 2.14.0, patching GHSA-ffc3-869f-jxw9
diff --git a/prowler/changelog.d/pymongo-4-18-2.security.md b/prowler/changelog.d/pymongo-4-18-2.security.md
deleted file mode 100644
index b621313d25..0000000000
--- a/prowler/changelog.d/pymongo-4-18-2.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749
diff --git a/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md b/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md
deleted file mode 100644
index 0a54879ed0..0000000000
--- a/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the SDK container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy
diff --git a/prowler/changelog.d/sdk-image-trivy-0-76-0.security.md b/prowler/changelog.d/sdk-image-trivy-0-76-0.security.md
new file mode 100644
index 0000000000..99fd4c2c70
--- /dev/null
+++ b/prowler/changelog.d/sdk-image-trivy-0-76-0.security.md
@@ -0,0 +1 @@
+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
diff --git a/prowler/changelog.d/urllib3-2-8-0.security.md b/prowler/changelog.d/urllib3-2-8-0.security.md
deleted file mode 100644
index 4645bb5942..0000000000
--- a/prowler/changelog.d/urllib3-2-8-0.security.md
+++ /dev/null
@@ -1 +0,0 @@
-`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689
diff --git a/prowler/compliance/gcp/pci_4.0_gcp.json b/prowler/compliance/gcp/pci_4.0_gcp.json
index e8e30a97e1..6bb7cd000d 100644
--- a/prowler/compliance/gcp/pci_4.0_gcp.json
+++ b/prowler/compliance/gcp/pci_4.0_gcp.json
@@ -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.",
diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json
index e7bf650ac7..5d5f024d71 100644
--- a/prowler/compliance/m365/iso27001_2022_m365.json
+++ b/prowler/compliance/m365/iso27001_2022_m365.json
@@ -174,7 +174,9 @@
}
],
"Checks": [
+ "entra_app_registration_redirect_uris_secure",
"entra_conditional_access_policy_app_enforced_restrictions",
+ "entra_cross_tenant_access_default_outbound_restricted",
"exchange_transport_config_smtp_auth_disabled",
"exchange_transport_rules_mail_forwarding_disabled",
"exchange_transport_rules_whitelist_disabled",
@@ -207,6 +209,8 @@
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_conditional_access_policy_directory_sync_account_excluded",
"entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced",
+ "entra_cross_tenant_access_default_outbound_restricted",
+ "entra_dynamic_group_member_of_rule_not_used",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
]
@@ -256,6 +260,7 @@
"entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
+ "entra_policy_admin_self_service_password_reset_disabled",
"entra_seamless_sso_disabled",
"entra_service_principal_no_secrets_for_permanent_tier0_roles",
"entra_users_mfa_enabled",
@@ -279,6 +284,7 @@
"Checks": [
"entra_admin_portals_access_restriction",
"entra_app_registration_no_unused_privileged_permissions",
+ "entra_dynamic_group_member_of_rule_not_used",
"entra_policy_guest_users_access_restrictions",
"entra_service_principal_no_secrets_for_permanent_tier0_roles",
"sharepoint_external_sharing_managed",
@@ -672,8 +678,10 @@
"entra_break_glass_account_fido2_security_key_registered",
"entra_emergency_access_exclusion",
"entra_app_registration_no_unused_privileged_permissions",
+ "entra_policy_admin_self_service_password_reset_disabled",
"entra_policy_ensure_default_user_cannot_create_tenants",
"entra_policy_guest_invite_only_for_admin_roles",
+ "entra_policy_msol_powershell_blocked",
"entra_seamless_sso_disabled",
"entra_service_principal_no_secrets_for_permanent_tier0_roles"
]
@@ -731,6 +739,7 @@
"entra_emergency_access_exclusion",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",
+ "entra_policy_admin_self_service_password_reset_disabled",
"entra_seamless_sso_disabled",
"entra_service_principal_no_secrets_for_permanent_tier0_roles",
"entra_users_mfa_enabled"
@@ -830,6 +839,7 @@
],
"Checks": [
"entra_app_registration_no_unused_privileged_permissions",
+ "entra_policy_msol_powershell_blocked",
"entra_policy_restricts_user_consent_for_apps",
"entra_thirdparty_integrated_apps_not_allowed",
"teams_external_domains_restricted",
@@ -925,6 +935,7 @@
"admincenter_users_admins_reduced_license_footprint",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"entra_app_registration_no_unused_privileged_permissions",
+ "entra_app_registration_redirect_uris_secure",
"entra_policy_restricts_user_consent_for_apps",
"entra_thirdparty_integrated_apps_not_allowed",
"sharepoint_modern_authentication_required"
diff --git a/prowler/config/config.py b/prowler/config/config.py
index aa85d98502..d9c86828e1 100644
--- a/prowler/config/config.py
+++ b/prowler/config/config.py
@@ -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.0"
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"
diff --git a/prowler/lib/network/__init__.py b/prowler/lib/network/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/lib/network/ssrf.py b/prowler/lib/network/ssrf.py
new file mode 100644
index 0000000000..691af46764
--- /dev/null
+++ b/prowler/lib/network/ssrf.py
@@ -0,0 +1,166 @@
+"""Outbound URL validation for provider connection tests."""
+
+from __future__ import annotations
+
+import ipaddress
+import os
+import re
+import socket
+from urllib.parse import urlparse
+
+from prowler.lib.logger import logger
+
+ALLOWED_PRIVATE_NETWORKS_ENV = "PROWLER_ALLOWED_PRIVATE_NETWORKS"
+
+# The check stays on unless a host process opts out. `prowler/__main__.py` opts the
+# CLI out, because there the operator and the person supplying the URL are the same
+# and there is no SSRF boundary to defend. Anything else -- Prowler App's API and
+# worker above all -- keeps the check, so a missing setting fails safe.
+SKIP_OUTBOUND_CHECK_ENV = "PROWLER_SKIP_OUTBOUND_HOST_CHECK"
+
+_TRUTHY = {"1", "true", "yes", "on"}
+
+_NON_PUBLIC_IP_PROPERTIES = (
+ "is_private",
+ "is_loopback",
+ "is_link_local",
+ "is_multicast",
+ "is_reserved",
+ "is_unspecified",
+)
+
+# scp-like git remotes (user@host:path) carry no scheme, so urlparse cannot read them
+_SCP_LIKE_REMOTE = re.compile(r"^(?:[^@/]+@)?(?P[^:/]+):(?!//)")
+
+_NAT64_WELL_KNOWN_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
+
+
+class OutboundURLNotAllowedError(Exception):
+ """A supplied URL points at a destination the worker must not reach."""
+
+
+def _parse_allowed_networks(raw: str | None) -> tuple:
+ if not raw or not raw.strip():
+ return ()
+ networks = []
+ for entry in raw.split(","):
+ entry = entry.strip()
+ if not entry:
+ continue
+ try:
+ networks.append(ipaddress.ip_network(entry, strict=False))
+ except ValueError as error:
+ raise OutboundURLNotAllowedError(
+ f"Malformed entry {entry!r} in {ALLOWED_PRIVATE_NETWORKS_ENV}: {error}"
+ )
+ return tuple(networks)
+
+
+def allowed_private_networks() -> tuple:
+ """Operator-configured private networks the SSRF guard must not block."""
+ networks = _parse_allowed_networks(os.environ.get(ALLOWED_PRIVATE_NETWORKS_ENV))
+ if networks:
+ logger.warning(
+ f"{ALLOWED_PRIVATE_NETWORKS_ENV} is set — SSRF protection relaxed for private networks: "
+ + ", ".join(str(network) for network in networks)
+ )
+ return networks
+
+
+def outbound_check_skipped() -> bool:
+ """Whether this process asked to skip the non-public destination check."""
+ return os.environ.get(SKIP_OUTBOUND_CHECK_ENV, "").strip().lower() in _TRUTHY
+
+
+def _unwrap_ipv6(address: ipaddress._BaseAddress) -> ipaddress._BaseAddress:
+ if not isinstance(address, ipaddress.IPv6Address):
+ return address
+ embedded = address.ipv4_mapped or address.sixtofour
+ if embedded is None and address in _NAT64_WELL_KNOWN_PREFIX:
+ embedded = ipaddress.IPv4Address(int(address) & 0xFFFFFFFF)
+ return embedded or address
+
+
+def _ip_is_non_public(address: str) -> bool:
+ try:
+ parsed = _unwrap_ipv6(ipaddress.ip_address(address))
+ except ValueError:
+ return False
+ # is_global is the broad check; the properties stay because some multicast
+ # ranges report is_global and would otherwise slip through
+ if not parsed.is_global:
+ return True
+ return any(getattr(parsed, prop) for prop in _NON_PUBLIC_IP_PROPERTIES)
+
+
+def _ip_is_allowlisted(address: str, networks: tuple) -> bool:
+ try:
+ parsed = ipaddress.ip_address(address)
+ except ValueError:
+ return False
+ return any(
+ parsed.version == network.version and parsed in network for network in networks
+ )
+
+
+def _resolve(host: str) -> set:
+ try:
+ return {sockaddr[0] for *_, sockaddr in socket.getaddrinfo(host, None)}
+ except socket.gaierror as error:
+ raise OutboundURLNotAllowedError(f"Could not resolve host {host!r}: {error}")
+
+
+def extract_host(url: str) -> str:
+ """Host of a URL, accepting scp-like git remotes that carry no scheme."""
+ scp_like = _SCP_LIKE_REMOTE.match(url)
+ if scp_like and "://" not in url:
+ return scp_like.group("host")
+ host = urlparse(url).hostname
+ if not host:
+ # The URL itself stays out of the message: a configured repository or auth
+ # URL can carry credentials in its userinfo, and this message is logged
+ raise OutboundURLNotAllowedError("Could not read a host from the supplied URL")
+ return host
+
+
+def validate_outbound_host(host: str) -> None:
+ """Reject a host that is, or resolves to, a non-public address.
+
+ Resolution happens here and again inside the client that connects, so a
+ hostile DNS server can still answer differently the second time.
+ """
+ if outbound_check_skipped():
+ logger.warning(
+ f"{SKIP_OUTBOUND_CHECK_ENV} is set — destination check disabled for host {host!r}"
+ )
+ return
+
+ networks = allowed_private_networks()
+
+ try:
+ ipaddress.ip_address(host)
+ except ValueError:
+ addresses = _resolve(host)
+ else:
+ addresses = {host}
+
+ for address in addresses:
+ if _ip_is_non_public(address) and not _ip_is_allowlisted(address, networks):
+ raise OutboundURLNotAllowedError(
+ f"Host {host!r} resolves to non-public address {address} and cannot be "
+ f"reached. To scan a target on a private network, list the trusted "
+ f"ranges in the {ALLOWED_PRIVATE_NETWORKS_ENV} environment variable of "
+ f"the process running the scan"
+ )
+
+
+def validate_outbound_url(
+ url: str, *, allowed_schemes: tuple = ("http", "https")
+) -> None:
+ """Reject a URL whose scheme is not allowed or whose host is not public."""
+ scheme = urlparse(url).scheme
+ if scheme and scheme not in allowed_schemes:
+ raise OutboundURLNotAllowedError(
+ f"Disallowed URL scheme {scheme!r}. Allowed: {', '.join(allowed_schemes)}"
+ )
+ validate_outbound_host(extract_host(url))
diff --git a/prowler/lib/outputs/compliance/compliance_output.py b/prowler/lib/outputs/compliance/compliance_output.py
index f4f84561f4..60fc5d6749 100644
--- a/prowler/lib/outputs/compliance/compliance_output.py
+++ b/prowler/lib/outputs/compliance/compliance_output.py
@@ -6,6 +6,7 @@ from prowler.lib.check.compliance_models import Compliance
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 sanitize_csv_value
class ComplianceOutput(Output):
@@ -83,7 +84,10 @@ class ComplianceOutput(Output):
csv_writer.writeheader()
for finding in self._data:
csv_writer.writerow(
- {k.upper(): v for k, v in finding.dict().items()}
+ {
+ k.upper(): sanitize_csv_value(v)
+ for k, v in finding.dict().items()
+ }
)
if self.close_file or self._from_cli:
self._file_descriptor.close()
diff --git a/prowler/lib/outputs/compliance/universal/universal_output.py b/prowler/lib/outputs/compliance/universal/universal_output.py
index 9c7c76da47..505e855b57 100644
--- a/prowler/lib/outputs/compliance/universal/universal_output.py
+++ b/prowler/lib/outputs/compliance/universal/universal_output.py
@@ -11,6 +11,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 sanitize_csv_value
from prowler.lib.utils.utils import open_file
if TYPE_CHECKING:
@@ -318,7 +319,12 @@ class UniversalComplianceOutput:
if self._file_descriptor.tell() == 0:
csv_writer.writeheader()
for row in self._data:
- csv_writer.writerow({k.upper(): v for k, v in row.dict().items()})
+ csv_writer.writerow(
+ {
+ k.upper(): sanitize_csv_value(v)
+ for k, v in row.dict().items()
+ }
+ )
if self.close_file or self._from_cli:
self._file_descriptor.close()
except Exception as error:
diff --git a/prowler/lib/outputs/csv/csv.py b/prowler/lib/outputs/csv/csv.py
index bcb50d9433..1c10b20af7 100644
--- a/prowler/lib/outputs/csv/csv.py
+++ b/prowler/lib/outputs/csv/csv.py
@@ -4,7 +4,7 @@ from typing import List
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, unroll_list
+from prowler.lib.outputs.utils import sanitize_csv_value, unroll_dict, unroll_list
class CSV(Output):
@@ -106,7 +106,9 @@ class CSV(Output):
if self._file_descriptor.tell() == 0:
csv_writer.writeheader()
for finding in self._data:
- csv_writer.writerow(finding)
+ csv_writer.writerow(
+ {k: sanitize_csv_value(v) for k, v in finding.items()}
+ )
if self.close_file or self._from_cli:
self._file_descriptor.close()
except Exception as error:
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index 60428bede7..da3faa8b1c 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -116,7 +116,7 @@ class HTML(Output):