Compare commits

..
19 Commits
Author SHA1 Message Date
Prowler BotandPepe Fagoaga 0d00e29608 chore(sdk): changelog for v5.30.3 (#11652)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-06-19 15:26:23 +02:00
Adrián PeñaandPepe Fagoaga f5ff30ad17 fix(saml): cross-tenant account takeover via SAML domain claiming (#11650)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-06-19 14:27:06 +02:00
Prowler BotandPedro Martín f6679fadf4 fix(compliance): multi-section undercount & leaked provider tab (#11635)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-06-18 10:40:20 +02:00
Prowler Botandprowler-bot dcc9401957 chore(release): Bump versions to v5.30.3 (#11627)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-06-17 10:47:31 +02:00
Prowler BotandPepe Fagoaga 88f6848913 chore(changelog): v5.30.2 (#11625)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-06-17 09:42:05 +02:00
Prowler Botandlydiavilchez 2de298fb7b fix(cli): prevent unrelated built-in provider failures from aborting the CLI (#11620)
Co-authored-by: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com>
2026-06-16 14:35:15 +02:00
11f0845a91 fix(gcp): surface organization-scan failures instead of silently scanning the home project (#11619)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2026-06-16 14:12:57 +02:00
Prowler BotandPedro Martín 42d99a17a6 perf(api): optimize scan-compliance-overviews task (#11613)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-06-16 12:18:55 +02:00
César Arroba 832f10b7f6 ci: narrow osv-scanner gate to CRITICAL on v5.30 (backport #11580) (#11616) 2026-06-16 11:35:30 +02:00
César Arroba d133ad18a4 ci: always run container and dependency vulnerability scans on PRs (v5.30 backport) (#11614) 2026-06-16 11:16:38 +02:00
3539940a26 fix(gcp): credit audit-filtered aggregated sinks in metric-filter checks (#11607)
Co-authored-by: Aline Almeida <aline@tuplita.ai>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
2026-06-16 10:34:23 +02:00
Prowler Botandprowler-bot 1192d94648 chore(release): Bump versions to v5.30.2 (#11571)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-06-12 13:48:44 +02:00
Prowler BotandJosema Camacho a578f4af34 chore: prepare API and UI changelogs for 5.30.1 release (#11566)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-06-12 12:16:15 +02:00
Prowler BotandAlejandro Bailo d6528b674e fix(ui): show threat map data for okta and google workspace accounts (#11563)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2026-06-12 10:18:43 +02:00
Prowler BotandJosema Camacho 75decbbedf fix(api): drop_subgraph deletes relationships then nodes to cut Neo4j memory (#11561)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-06-12 09:47:41 +02:00
Prowler BotandPedro Martín 4a14559a5f fix(compliance): resolve provider from scan in attributes endp (#11560)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2026-06-12 09:18:11 +02:00
Prowler BotandHugo Pereira Brito c6f8620a0d fix(api): normalize OCI scan region credentials (#11559)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-06-11 17:55:26 +02:00
Prowler Botandprowler-bot ca4889b43e chore(release): Bump versions to v5.30.1 (#11547)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-06-11 15:28:54 +02:00
Prowler Botandprowler-bot 057d061c7e chore(api): Update prowler dependency to v5.30 for release 5.30.0 (#11543)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-06-11 11:15:18 +02:00
725 changed files with 5714 additions and 51268 deletions
+5 -17
View File
@@ -6,20 +6,14 @@
PROWLER_UI_VERSION="stable"
AUTH_URL=http://localhost:3000
API_BASE_URL=http://prowler-api:8080/api/v1
# deprecated, use UI_API_BASE_URL
NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL}
UI_API_BASE_URL=${API_BASE_URL}
# deprecated, use UI_API_DOCS_URL
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
UI_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST=true
UI_PORT=3000
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID (empty/unset ⇒ GTM not loaded, zero egress)
# deprecated, use UI_GOOGLE_TAG_MANAGER_ID
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
UI_GOOGLE_TAG_MANAGER_ID=""
#### MCP Server ####
PROWLER_MCP_VERSION=stable
@@ -145,19 +139,13 @@ DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_SENTRY_DSN=
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
# Sentry for the web app (server + browser). Empty/unset UI_SENTRY_DSN ⇒
# Sentry disabled, zero egress. SENTRY_RELEASE (unprefixed) feeds the web app's
# server/edge SDKs.
UI_SENTRY_DSN=
UI_SENTRY_ENVIRONMENT=local
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
# Reserved runtime public config (registered now; no UI consumer yet)
# POSTHOG_KEY=
# POSTHOG_HOST=
# REO_DEV_CLIENT_ID=
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.31.2
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.30.3
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
+1
View File
@@ -1,6 +1,7 @@
# SDK
/* @prowler-cloud/detection-remediation
/prowler/ @prowler-cloud/detection-remediation
/prowler/compliance/ @prowler-cloud/compliance
/tests/ @prowler-cloud/detection-remediation
/dashboard/ @prowler-cloud/detection-remediation
/docs/ @prowler-cloud/detection-remediation
+2 -20
View File
@@ -43,17 +43,8 @@ runs:
if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
LATEST_COMMIT=$(curl -sf \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/prowler-cloud/prowler/commits/master" \
| jq -er '.sha') || {
echo "::error::Failed to fetch latest prowler/master commit from the GitHub API (HTTP error or missing .sha). Check the GITHUB_TOKEN and API rate limits."
exit 1
}
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
echo "Latest commit hash: $LATEST_COMMIT"
sed -i "s|\(git = \"https://github\.com/prowler-cloud/prowler\.git?rev=master\)#[a-f0-9]\{40\}\"|\1#${LATEST_COMMIT}\"|g" uv.lock
echo "Updated uv.lock entry:"
@@ -63,17 +54,8 @@ runs:
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'prowler-cloud/prowler'
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
LATEST_COMMIT=$(curl -sf \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/prowler-cloud/prowler/commits/master" \
| jq -er '.sha') || {
echo "::error::Failed to fetch latest prowler/master commit from the GitHub API (HTTP error or missing .sha). Check the GITHUB_TOKEN and API rate limits."
exit 1
}
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
echo "Latest commit hash: $LATEST_COMMIT"
sed -i "s|\(git = \"https://github\.com/prowler-cloud/prowler\.git?rev=master\)#[a-f0-9]\{40\}\"|\1#${LATEST_COMMIT}\"|g" uv.lock
echo "Updated uv.lock entry:"
+2 -2
View File
@@ -63,7 +63,7 @@ runs:
exit-code: '0'
scanners: 'vuln'
timeout: '5m'
version: 'v0.71.0'
version: 'v0.69.2'
- name: Run Trivy vulnerability scan (SARIF)
if: inputs.upload-sarif == 'true' && github.event_name == 'push'
@@ -76,7 +76,7 @@ runs:
exit-code: '0'
scanners: 'vuln'
timeout: '5m'
version: 'v0.71.0'
version: 'v0.69.2'
- name: Upload Trivy results to GitHub Security tab
if: inputs.upload-sarif == 'true' && github.event_name == 'push'
-5
View File
@@ -77,11 +77,6 @@ provider/okta:
- any-glob-to-any-file: "prowler/providers/okta/**"
- any-glob-to-any-file: "tests/providers/okta/**"
provider/linode:
- changed-files:
- any-glob-to-any-file: "prowler/providers/linode/**"
- any-glob-to-any-file: "tests/providers/linode/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
+3 -2
View File
@@ -6,7 +6,8 @@
# - .github/workflows/api-security.yml, sdk-security.yml, ui-security.yml
#
# Severity levels (comma-separated) are read from OSV_SEVERITY_LEVELS.
# Default: CRITICAL — only CVSS >= 9.0 findings fail the scan.
# Default: HIGH,CRITICAL,UNKNOWN — preserves prior .safety-policy.yml policy
# (ignore-cvss-severity-below: 7 + ignore-cvss-unknown-severity: False).
# osv-scanner has no native CVSS threshold (google/osv-scanner#1400, closed
# not-planned). Severity is derived from $group.max_severity (numeric CVSS
# score string) which osv-scanner emits per group.
@@ -32,7 +33,7 @@ set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
CONFIG="${ROOT}/osv-scanner.toml"
SEVERITY_LEVELS="${OSV_SEVERITY_LEVELS:-CRITICAL}"
SEVERITY_LEVELS="${OSV_SEVERITY_LEVELS:-HIGH,CRITICAL,UNKNOWN}"
for bin in osv-scanner jq; do
if ! command -v "${bin}" >/dev/null 2>&1; then
+1 -1
View File
@@ -131,5 +131,5 @@ jobs:
with:
image-name: ${{ env.IMAGE_NAME }}
image-tag: ${{ github.sha }}
fail-on-critical: 'true'
fail-on-critical: 'false'
severity: 'CRITICAL'
+1 -1
View File
@@ -124,5 +124,5 @@ jobs:
with:
image-name: ${{ env.IMAGE_NAME }}
image-tag: ${{ github.sha }}
fail-on-critical: 'true'
fail-on-critical: 'false'
severity: 'CRITICAL'
+1
View File
@@ -24,6 +24,7 @@ permissions: {}
jobs:
mcp-security-scans:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
-1
View File
@@ -29,7 +29,6 @@ jobs:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
steps:
- name: Harden Runner
+18 -7
View File
@@ -105,14 +105,25 @@ jobs:
id: check-changes
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
prowler/**
Dockerfile*
pyproject.toml
uv.lock
.github/workflows/sdk-container-checks.yml
files: ./**
files_ignore: |
.github/**
prowler/CHANGELOG.md
docs/**
permissions/**
api/**
ui/**
dashboard/**
mcp_server/**
skills/**
README.md
mkdocs.yml
.backportrc.json
.env
docker-compose*
examples/**
.gitignore
contrib/**
**/AGENTS.md
- name: Set up Docker Buildx
@@ -136,5 +147,5 @@ jobs:
with:
image-name: ${{ env.IMAGE_NAME }}
image-tag: ${{ github.sha }}
fail-on-critical: 'true'
fail-on-critical: 'false'
severity: 'CRITICAL'
+18 -9
View File
@@ -61,18 +61,27 @@ jobs:
id: check-changes
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
prowler/**
tests/**
pyproject.toml
uv.lock
.github/workflows/sdk-tests.yml
files:
./**
.github/workflows/sdk-security.yml
.github/actions/setup-python-uv/**
.github/actions/osv-scanner/**
.github/scripts/osv-scan.sh
files_ignore: |
.github/**
prowler/CHANGELOG.md
docs/**
permissions/**
api/**
ui/**
dashboard/**
mcp_server/**
skills/**
README.md
mkdocs.yml
.backportrc.json
.env
docker-compose*
examples/**
.gitignore
contrib/**
**/AGENTS.md
- name: Setup Python with uv
+4 -29
View File
@@ -29,7 +29,6 @@ jobs:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
steps:
- name: Harden Runner
@@ -541,7 +540,7 @@ jobs:
with:
flags: prowler-py${{ matrix.python-version }}-vercel
files: ./vercel_coverage.xml
# Scaleway Provider
- name: Check if Scaleway files changed
if: steps.check-changes.outputs.any_changed == 'true'
@@ -589,31 +588,7 @@ jobs:
with:
flags: prowler-py${{ matrix.python-version }}-stackit
files: ./stackit_coverage.xml
# Linode Provider
- name: Check if Linode files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-linode
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
./prowler/**/linode/**
./tests/**/linode/**
./uv.lock
- name: Run Linode tests
if: steps.changed-linode.outputs.any_changed == 'true'
run: uv run pytest -n auto --cov=./prowler/providers/linode --cov-report=xml:linode_coverage.xml tests/providers/linode
- name: Upload Linode coverage to Codecov
if: steps.changed-linode.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 }}-linode
files: ./linode_coverage.xml
# External Provider (dynamic loading)
- name: Check if External Provider files changed
if: steps.check-changes.outputs.any_changed == 'true'
@@ -633,14 +608,14 @@ jobs:
- name: Upload External Provider coverage to Codecov
if: steps.changed-external.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 }}-external
files: ./external_coverage.xml
# Lib
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'
@@ -32,6 +32,9 @@ env:
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
# Build args
NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1
permissions: {}
jobs:
@@ -143,6 +146,7 @@ jobs:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && format('v{0}', env.RELEASE_TAG) || needs.setup.outputs.short-sha }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
push: true
platforms: ${{ matrix.platform }}
tags: |
+1 -1
View File
@@ -129,5 +129,5 @@ jobs:
with:
image-name: ${{ env.IMAGE_NAME }}
image-tag: ${{ github.sha }}
fail-on-critical: 'true'
fail-on-critical: 'false'
severity: 'CRITICAL'
+2 -11
View File
@@ -40,8 +40,7 @@ jobs:
AUTH_SECRET: 'fallback-ci-secret-for-testing'
AUTH_TRUST_HOST: true
NEXTAUTH_URL: 'http://localhost:3000'
AUTH_URL: 'http://localhost:3000'
UI_API_BASE_URL: 'http://localhost:8080/api/v1'
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }}
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }}
@@ -78,14 +77,6 @@ jobs:
E2E_ALIBABACLOUD_ACCESS_KEY_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_ID }}
E2E_ALIBABACLOUD_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET }}
E2E_ALIBABACLOUD_ROLE_ARN: ${{ secrets.E2E_ALIBABACLOUD_ROLE_ARN }}
E2E_OKTA_DOMAIN: ${{ secrets.E2E_OKTA_DOMAIN }}
E2E_OKTA_CLIENT_ID: ${{ secrets.E2E_OKTA_CLIENT_ID }}
E2E_OKTA_BASE64_PRIVATE_KEY: ${{ secrets.E2E_OKTA_BASE64_PRIVATE_KEY }}
E2E_GOOGLEWORKSPACE_CUSTOMER_ID: ${{ secrets.E2E_GOOGLEWORKSPACE_CUSTOMER_ID }}
E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON: ${{ secrets.E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON }}
E2E_GOOGLEWORKSPACE_DELEGATED_USER: ${{ secrets.E2E_GOOGLEWORKSPACE_DELEGATED_USER }}
E2E_VERCEL_TEAM_ID: ${{ secrets.E2E_VERCEL_TEAM_ID }}
E2E_VERCEL_API_TOKEN: ${{ secrets.E2E_VERCEL_API_TOKEN }}
# Pass E2E paths from impact analysis
E2E_TEST_PATHS: ${{ needs.impact-analysis.outputs.ui-e2e }}
RUN_ALL_TESTS: ${{ needs.impact-analysis.outputs.run-all }}
@@ -166,7 +157,7 @@ jobs:
timeout=150
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s ${UI_API_BASE_URL}/docs >/dev/null 2>&1; then
if curl -s ${NEXT_PUBLIC_API_BASE_URL}/docs >/dev/null 2>&1; then
echo "Prowler API is ready!"
exit 0
fi
+1
View File
@@ -24,6 +24,7 @@ permissions: {}
jobs:
ui-security-scans:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
-4
View File
@@ -131,10 +131,6 @@ jobs:
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run healthcheck
- name: Check product-tour alignment
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run tour:check
- name: Run pnpm audit
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run audit
-8
View File
@@ -7,10 +7,6 @@
# P50 — dependency validation
default_install_hook_types: [pre-commit]
# Hooks run on commit only by default;
# NOTE: default_stages does NOT override a hook's manifest stages, so fixers shipping pre-push in their
# manifest need an explicit stages: ["pre-commit"] below to stay off push.
default_stages: [pre-commit]
repos:
## GENERAL (prek built-in — no external repo needed)
@@ -25,16 +21,13 @@ repos:
- id: check-json
priority: 10
- id: end-of-file-fixer
stages: ["pre-commit"]
priority: 0
- id: trailing-whitespace
stages: ["pre-commit"]
priority: 0
- id: no-commit-to-branch
priority: 10
- id: pretty-format-json
args: ["--autofix", --no-sort-keys, --no-ensure-ascii]
stages: ["pre-commit"]
priority: 10
## TOML
@@ -89,7 +82,6 @@ repos:
name: "SDK - isort"
files: { glob: ["{prowler,tests,dashboard,util,scripts}/**/*.py"] }
args: ["--profile", "black"]
stages: ["pre-commit"]
priority: 20
- repo: https://github.com/psf/black
-85
View File
@@ -1,85 +0,0 @@
# Trivy ignore file for prowlercloud/prowler SDK container image.
# Each entry below documents (a) the affected package and why it ships in the
# image, (b) why the CVE is not exploitable in Prowler's runtime, and (c) the
# upstream fix status. Entries carry an expiry so they auto-force re-review.
# Entries are scoped per-package so suppressions cannot drift onto unrelated
# packages that may be assigned the same CVE in the future.
#
# Scanned by: .github/actions/trivy-scan via .github/workflows/sdk-container-checks.yml
# CVE-2026-42496 — perl-archive-tar path traversal via crafted symlinks.
# CVE-2026-8376 — perl heap buffer overflow when compiling regex.
# Packages: perl, perl-base, perl-modules-5.36, libperl5.36.
# Why ignored: perl-base is part of Debian's "Essential: yes" set; it cannot be
# removed without breaking dpkg. The Prowler SDK does not invoke perl at runtime;
# neither vulnerable code path (Archive::Tar parsing or regex compilation of
# attacker-controlled input) is reachable from Prowler. No Debian bookworm fix
# is available yet.
CVE-2026-42496 pkg:perl exp:2026-07-15
CVE-2026-42496 pkg:perl-base exp:2026-07-15
CVE-2026-42496 pkg:perl-modules-5.36 exp:2026-07-15
CVE-2026-42496 pkg:libperl5.36 exp:2026-07-15
CVE-2026-8376 pkg:perl exp:2026-07-15
CVE-2026-8376 pkg:perl-base exp:2026-07-15
CVE-2026-8376 pkg:perl-modules-5.36 exp:2026-07-15
CVE-2026-8376 pkg:libperl5.36 exp:2026-07-15
# CVE-2025-7458 — SQLite integer overflow.
# Package: libsqlite3-0.
# Why ignored: transitive dependency of CPython's stdlib sqlite3 module. The
# Prowler SDK does not open user-supplied SQLite databases; SQLite usage is
# internal and bounded. No Debian bookworm fix is available.
CVE-2025-7458 pkg:libsqlite3-0 exp:2026-07-15
# CVE-2026-43185 — Linux kernel ksmbd signedness bug.
# Package: linux-libc-dev.
# Why ignored: linux-libc-dev ships kernel headers for build-time compilation,
# not a running kernel. Containers execute against the host kernel, so these
# headers are inert at runtime. The upstream fix landed in kernel 7.0-rc2 and
# has not been backported to Debian's 6.1 LTS line.
CVE-2026-43185 pkg:linux-libc-dev exp:2026-07-15
# CVE-2023-45853 — zlib MiniZip integer overflow / heap overflow in
# zipOpenNewFileInZip4_64.
# Packages: zlib1g, zlib1g-dev.
# Why ignored: Debian Security Tracker status for bookworm is <ignored>, with
# the published rationale "contrib/minizip not built and src:zlib not producing
# binary packages" — i.e. the vulnerable symbol is not present in the libz.so
# shipped by Debian. Real-not-affected, not unpatched. Upstream fix is in
# zlib 1.3.1, available in Debian trixie (13); migrating the base image would
# clear it fully.
# Ref: https://security-tracker.debian.org/tracker/CVE-2023-45853
CVE-2023-45853 pkg:zlib1g exp:2026-07-15
CVE-2023-45853 pkg:zlib1g-dev exp:2026-07-15
# --- API container image (api/Dockerfile) ---
# The entries below are specific to the Prowler API image, which ships
# PowerShell and additional build tooling on top of the same bookworm base.
# CVE-2026-7210 — CPython/Expat hash-flooding denial of service in
# `xml.parsers.expat` and `xml.etree.ElementTree`.
# Packages: the Debian system Python 3.11 (python3.11*, libpython3.11*).
# Why ignored: the API runs under the Python 3.12 interpreter shipped in its
# `.venv`; the system `python3.11` is only present because `python3-dev` is
# pulled in to compile native extensions (xmlsec, lxml) and is never executed
# at runtime. The vulnerable path requires parsing attacker-controlled XML with
# the affected interpreter, which Prowler does not do with the system Python.
# Full mitigation also needs libexpat >= 2.8.0; no Debian bookworm fix yet.
CVE-2026-7210 pkg:python3.11 exp:2026-07-15
CVE-2026-7210 pkg:python3.11-dev exp:2026-07-15
CVE-2026-7210 pkg:python3.11-minimal exp:2026-07-15
CVE-2026-7210 pkg:libpython3.11 exp:2026-07-15
CVE-2026-7210 pkg:libpython3.11-dev exp:2026-07-15
CVE-2026-7210 pkg:libpython3.11-minimal exp:2026-07-15
CVE-2026-7210 pkg:libpython3.11-stdlib exp:2026-07-15
# CVE-2026-33278 — Unbound DNSSEC validator use-after-free (DoS, possible RCE).
# CVE-2026-42960 — Unbound DNS cache poisoning via promiscuous additional records.
# Package: libunbound8.
# Why ignored: libunbound8 is a transitive apt dependency of the TLS/networking
# stack (GnuTLS DANE support); only the shared library ships in the image. Both
# vulnerabilities require operating a live Unbound recursive DNSSEC validator
# that processes attacker-influenced DNS responses. Prowler never starts an
# Unbound resolver, so neither code path is reachable. No Debian bookworm fix yet.
CVE-2026-33278 pkg:libunbound8 exp:2026-07-15
CVE-2026-42960 pkg:libunbound8 exp:2026-07-15
-6
View File
@@ -51,7 +51,6 @@ Use these skills for detailed patterns on-demand:
| `django-migration-psql` | Django migration best practices for PostgreSQL | [SKILL.md](skills/django-migration-psql/SKILL.md) |
| `postgresql-indexing` | PostgreSQL indexing, EXPLAIN, monitoring, maintenance | [SKILL.md](skills/postgresql-indexing/SKILL.md) |
| `prowler-attack-paths-query` | Create Attack Paths openCypher queries | [SKILL.md](skills/prowler-attack-paths-query/SKILL.md) |
| `prowler-tour` | Keep product-tour definitions aligned with the UI | [SKILL.md](skills/prowler-tour/SKILL.md) |
| `gh-aw` | GitHub Agentic Workflows (gh-aw) | [SKILL.md](skills/gh-aw/SKILL.md) |
| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
@@ -68,12 +67,10 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Adding new providers | `prowler-provider` |
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
| Adding services to existing providers | `prowler-provider` |
| Adding, updating, or removing a tour definition (*.tour.ts) | `prowler-tour` |
| After creating/modifying a skill | `skill-sync` |
| App Router / Server Actions | `nextjs-16` |
| Auditing check-to-requirement mappings as a cloud auditor | `prowler-compliance` |
| Building AI chat features | `ai-sdk-5` |
| Changing button labels or section headings on a tour-covered page | `prowler-tour` |
| Committing changes | `prowler-commit` |
| Configuring MCP servers in agentic workflows | `gh-aw` |
| Create PR that requires changelog entry | `prowler-changelog` |
@@ -92,7 +89,6 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Creating/updating compliance frameworks | `prowler-compliance` |
| Debug why a GitHub Actions job is failing | `prowler-ci` |
| Debugging gh-aw compilation errors | `gh-aw` |
| Editing a UI file containing data-tour-id attributes | `prowler-tour` |
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
| Fixing bug | `tdd` |
| Fixing compliance JSON bugs (duplicate IDs, empty Section, stale refs) | `prowler-compliance` |
@@ -109,8 +105,6 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Modifying gh-aw workflow frontmatter or safe-outputs | `gh-aw` |
| Refactoring code | `tdd` |
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
| Renaming or removing a data-tour-id attribute value | `prowler-tour` |
| Restructuring routes or layouts covered by a tour | `prowler-tour` |
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
+2 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf AS build
FROM python:3.12.11-slim-bookworm@sha256:519591d6871b7bc437060736b9f7456b8731f1499a57e22e6c285135ae657bf7 AS build
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
@@ -6,7 +6,7 @@ LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
ARG POWERSHELL_VERSION=7.5.0
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
ARG TRIVY_VERSION=0.71.0
ARG TRIVY_VERSION=0.70.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
-29
View File
@@ -1,34 +1,5 @@
.DEFAULT_GOAL:=help
DEV_LOCAL := ./scripts/development/dev-local.sh
.PHONY: dev dev-setup dev-attach dev-launch dev-stop dev-clean dev-wipe dev-status
##@ Local Development
dev: ## Start local API, worker, and database logs
$(DEV_LOCAL) all
dev-setup: ## Bootstrap local dependencies, migrations, and fixtures
$(DEV_LOCAL) setup
dev-attach: ## Attach to the local tmux development session
$(DEV_LOCAL) attach
dev-launch: ## Start the local stack on fixed ports and attach
$(DEV_LOCAL) launch
dev-stop: ## Stop the local tmux session and containers
$(DEV_LOCAL) kill
dev-clean: ## Remove stopped local development containers
$(DEV_LOCAL) clean
dev-wipe: ## Stop everything and delete local development data
$(DEV_LOCAL) wipe
dev-status: ## Show local development container status
$(DEV_LOCAL) status
##@ Testing
test: ## Test with pytest
rm -rf .coverage && \
-1
View File
@@ -121,7 +121,6 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| OpenStack | 34 | 5 | 0 | 9 | Official | UI, API, CLI |
| Vercel | 26 | 6 | 0 | 8 | Official | UI, API, CLI |
| Okta | 1 | 1 | 0 | 1 | Official | CLI |
| Linode [Contact us](https://prowler.com/contact) | 10 | 3 | 0 | 4 | Unofficial | CLI |
| Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 0 | 1 | Unofficial | CLI |
| StackIT [Contact us](https://prowler.com/contact) | 7 | 2 | 0 | 3 | Unofficial | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
-3
View File
@@ -24,9 +24,6 @@ DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
# Decide whether to allow Django manage database table partitions
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
# Optional: bound Celery's prefork pool size. Unset → Celery uses os.cpu_count().
# Useful on Kubernetes nodes with many CPUs where unbounded prefork balloons memory.
# DJANGO_CELERY_WORKER_CONCURRENCY=4
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_SENTRY_DSN=
+1 -46
View File
@@ -2,56 +2,11 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.32.2] (Prowler UNRELEASED)
### 🐞 Fixed
- `scan-perform` no longer reports an error when a provider is deleted during a running scan [(#11696)](https://github.com/prowler-cloud/prowler/pull/11696)
---
## [1.32.1] (Prowler v5.31.1)
### 🐞 Fixed
- API key auth no longer mutates `TenantAPIKey.objects` during admin DB lookups [(#11686)](https://github.com/prowler-cloud/prowler/pull/11686)
---
## [1.32.0] (Prowler v5.31.0)
### 🚀 Added
- Provider group filters for API endpoints that support cloud provider filtering, including exact and `__in` variants [(#11573)](https://github.com/prowler-cloud/prowler/pull/11573)
- Provider filters for `GET /api/v1/compliance-overviews`, `/metadata`, and `/requirements`, using latest completed scans per matching provider [(#11587)](https://github.com/prowler-cloud/prowler/pull/11587)
- Server-Sent Events (SSE) infrastructure for the API: a base viewset, a tenant-aware channel manager, and channel-name helpers backed by `django-eventstream` over Valkey Pub/Sub and served through the Gunicorn ASGI worker, so feature endpoints can stream events to clients over a single long-lived connection [(#11556)](https://github.com/prowler-cloud/prowler/pull/11556)
- `DJANGO_CELERY_WORKER_CONCURRENCY` to configure Celery workers concurrency. Unset for default behaviour [(#11075)](https://github.com/prowler-cloud/prowler/pull/11075)
### 🔄 Changed
- Gunicorn worker timeout raised from the 30s default to 120s, so long-running requests are no longer killed prematurely [(#11631)](https://github.com/prowler-cloud/prowler/pull/11631)
- Sentry now drops ASGI's `RequestAborted` errors from health-check probe disconnects on `/health/live` [(#11632)](https://github.com/prowler-cloud/prowler/pull/11632)
- Gunicorn keep-alive timeout now exceeds the load balancer idle timeout, stopping 502s from reused connections [(#11647)](https://github.com/prowler-cloud/prowler/pull/11647)
- API runs under the Uvicorn worker so keep-alive outlives the load balancer idle timeout, fixing Gunicorn's intermittent 502s [(#11663)](https://github.com/prowler-cloud/prowler/pull/11663)
- SAML logins no longer wipe a user's roles when the IdP does not send the `userType` attribute; existing roles are kept, and when `userType` names a role that does not exist it is now created with read-only access (visibility over all providers, no management permissions) instead of no permissions at all [(#11520)](https://github.com/prowler-cloud/prowler/pull/11520)
### 🐞 Fixed
- Database connections no longer leak under the ASGI worker, which previously exhausted the read replica's connection slots and caused 500s on read endpoints [(#11640)](https://github.com/prowler-cloud/prowler/pull/11640)
### 🔐 Security
- `aiohttp` to 3.14.0 and `idna` to 3.15, patching known CVEs [(#11596)](https://github.com/prowler-cloud/prowler/pull/11596)
- Container base image to `python:3.12.13-slim-bookworm` and `trivy` to 0.71.0, patching OS and Go module CVEs [(#11596)](https://github.com/prowler-cloud/prowler/pull/11596)
- `trivy` binary bumped to 0.71.0 patching embedded `golang.org/x/crypto`, `golang.org/x/net`, and Go `stdlib` CVEs [(#11592)](https://github.com/prowler-cloud/prowler/pull/11592)
---
## [1.31.3] (Prowler v5.30.3)
### 🔐 Security
- SAML logins now link to an existing account only when the asserted email domain matches the ACS endpoint and the user is already a member of that domain's tenant, fixing a cross-tenant account takeover [(GHSA-h8m9-jgf8-vwvp)](https://github.com/prowler-cloud/prowler/security/advisories/GHSA-h8m9-jgf8-vwvp)
- SAML logins now link to an existing account only when the asserted email domain matches the ACS endpoint and the user is already a member of that domain's tenant, fixing a cross-tenant account takeover [(GHSA-h8m9-jgf8-vwvp)](https://github.com/prowler-cloud/prowler/security/advisories/GHSA-h8m9-jgf8-vwvp) [bf3b5c2ba713e533014927141b64948c82c8f32e](https://github.com/prowler-cloud/prowler/commit/bf3b5c2ba713e533014927141b64948c82c8f32e)
---
+2 -2
View File
@@ -1,11 +1,11 @@
FROM python:3.12.13-slim-bookworm@sha256:76d4b7b6305788c6b4c6a19d6a22a3921bf802e9af4d5e1e5bd771208dba74bf AS build
FROM python:3.12.10-slim-bookworm@sha256:fd95fa221297a88e1cf49c55ec1828edd7c5a428187e67b5d1805692d11588db AS build
LABEL maintainer="https://github.com/prowler-cloud/api"
ARG POWERSHELL_VERSION=7.5.0
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
ARG TRIVY_VERSION=0.71.0
ARG TRIVY_VERSION=0.70.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
ARG ZIZMOR_VERSION=1.24.1
-36
View File
@@ -196,42 +196,6 @@ python -m celery -A config.celery worker -l info -E
The Celery worker does not detect and reload changes in the code, so you need to restart it manually when you make changes.
### Makefile-Assisted Local Deployment
This method is an additional local development workflow. It does not replace the manual local deployment or the Docker deployment described in this guide.
PostgreSQL, Valkey, and Neo4j run with Docker Compose, while Django and the Celery worker run natively through `uv`. Additionally, this workflow creates a `tmux` session with panes for the API, worker, and PostgreSQL logs.
Before using this method, ensure `docker compose`, `tmux`, and `uv` are installed.
This workflow is designed for macOS and should also work on Linux when Docker, `tmux`, and `uv` are available. Windows requires script changes before it can be supported.
From the repository root, run:
```console
make dev
```
The API will be available at:
```console
http://localhost:8080/api/v1
```
Use these commands to manage the local stack:
```console
make dev-setup # Bootstrap dependencies, migrations, and fixtures
make dev-attach # Attach to the tmux session
make dev-launch # Start the stack on fixed ports and attach
make dev-stop # Stop the tmux session and containers
make dev-clean # Remove stopped development containers
make dev-wipe # Stop everything and delete local development data
make dev-status # Show development container status
```
This workflow does not start the UI. Start it separately from the `ui/` directory when needed.
### Docker deployment
This method requires `docker` and `docker compose`.
+3 -9
View File
@@ -21,19 +21,13 @@ apply_fixtures() {
}
start_dev_server() {
echo "Starting the development server (Gunicorn ASGI, debug + reload)..."
# Same server/worker as prod (config.asgi via the native `asgi` worker), so
# SSE streams run on the event loop exactly as they do in production. DEBUG is
# on so guniconf's `reload = DEBUG` hot-reloads edited code (and flips
# `preload_app` off so reload actually takes).
export DJANGO_DEBUG="${DJANGO_DEBUG:-True}"
export DJANGO_BIND_ADDRESS="${DJANGO_BIND_ADDRESS:-0.0.0.0}"
exec uv run gunicorn -c config/guniconf.py config.asgi:application
echo "Starting the development server..."
exec uv run python manage.py runserver 0.0.0.0:"${DJANGO_PORT:-8080}"
}
start_prod_server() {
echo "Starting the Gunicorn server..."
exec uv run gunicorn -c config/guniconf.py config.asgi:application
exec uv run gunicorn -c config/guniconf.py config.wsgi:application
}
resolve_worker_hostname() {
-1
View File
@@ -65,7 +65,6 @@ All settings have safe defaults; override via environment variables.
| Env var | Default | Purpose |
| --- | --- | --- |
| `DJANGO_CELERY_WORKER_PREFETCH_MULTIPLIER` | `1` | Tasks reserved per worker process. |
| `DJANGO_CELERY_WORKER_CONCURRENCY` | unset | Optional Celery prefork pool size. When unset, Celery uses its CPU-based default. Set this on worker containers to bound idle memory on hosts with many CPUs. |
| `DJANGO_CELERY_WORKER_SOFT_SHUTDOWN_TIMEOUT` | `60` | Seconds the worker drains/re-queues on `SIGTERM` before force-kill. |
| `DJANGO_CELERY_TASK_TIME_LIMIT` | `21600` (6h) | Hard limit for most tasks; connection checks are capped at 120s. |
| `DJANGO_CELERY_TASK_SOFT_TIME_LIMIT` | hard - 600 | Soft limit; raises `SoftTimeLimitExceeded` for cleanup. |
+12 -17
View File
@@ -41,11 +41,9 @@ dependencies = [
"drf-spectacular==0.27.2",
"drf-spectacular-jsonapi==0.5.1",
"defusedxml==0.7.1",
"django-eventstream==5.3.3",
"gunicorn==26.0.0",
"uvloop==0.22.1",
"gunicorn==23.0.0",
"lxml==6.1.0",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.31",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.30",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (==1.3.0)",
"sentry-sdk[django] (==2.56.0)",
@@ -62,8 +60,7 @@ dependencies = [
"gevent (==25.9.1)",
"werkzeug (==3.1.7)",
"sqlparse (==0.5.5)",
"fonttools (==4.62.1)",
"uvicorn-worker (==0.4.0)",
"fonttools (==4.62.1)"
]
description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
@@ -71,7 +68,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.32.2"
version = "1.31.3"
[tool.uv]
# Transitive pins matching master to avoid silent drift; bump deliberately.
@@ -82,7 +79,7 @@ constraint-dependencies = [
"aiobotocore==2.25.1",
"aiofiles==24.1.0",
"aiohappyeyeballs==2.6.1",
"aiohttp==3.14.0",
"aiohttp==3.13.5",
"aioitertools==0.13.0",
"aiosignal==1.4.0",
"alibabacloud-actiontrail20200706==2.4.1",
@@ -127,8 +124,9 @@ constraint-dependencies = [
"astroid==3.2.4",
"async-timeout==5.0.1",
"attrs==25.4.0",
"authlib==1.6.12",
"authlib==1.6.9",
"autopep8==2.3.2",
"awsipranges==0.3.3",
"azure-cli-core==2.83.0",
"azure-cli-telemetry==1.1.0",
"azure-common==1.1.28",
@@ -211,7 +209,6 @@ constraint-dependencies = [
"django-celery-results==2.6.0",
"django-cors-headers==4.4.0",
"django-environ==0.11.2",
"django-eventstream==5.3.3",
"django-filter==24.3",
"django-guid==3.5.0",
"django-postgres-extra==2.0.9",
@@ -256,7 +253,7 @@ constraint-dependencies = [
"grpc-google-iam-v1==0.14.3",
"grpcio==1.76.0",
"grpcio-status==1.76.0",
"gunicorn==26.0.0",
"gunicorn==23.0.0",
"h11==0.16.0",
"h2==4.3.0",
"hpack==4.1.0",
@@ -265,8 +262,8 @@ constraint-dependencies = [
"httpx==0.28.1",
"humanfriendly==10.0",
"hyperframe==6.1.0",
"iamdata==0.1.202605131",
"idna==3.15",
"iamdata==0.1.202602021",
"idna==3.11",
"importlib-metadata==8.7.1",
"inflection==0.5.1",
"iniconfig==2.3.0",
@@ -318,7 +315,7 @@ constraint-dependencies = [
"neo4j==6.1.0",
"nest-asyncio==1.6.0",
"nltk==3.9.4",
"numpy==2.2.6",
"numpy==2.0.2",
"oauthlib==3.3.1",
"oci==2.169.0",
"openai==1.109.1",
@@ -347,7 +344,7 @@ constraint-dependencies = [
"psutil==7.2.2",
"psycopg2-binary==2.9.9",
"py-deviceid==0.1.1",
"py-iam-expand==0.3.0",
"py-iam-expand==0.1.0",
"py-ocsf-models==0.8.1",
"pyasn1==0.6.3",
"pyasn1-modules==0.4.2",
@@ -423,8 +420,6 @@ constraint-dependencies = [
"uritemplate==4.2.0",
"urllib3==2.7.0",
"uuid6==2024.7.10",
"uvicorn==0.49.0",
"uvloop==0.22.1",
"vine==5.1.0",
"vulture==2.14",
"wcwidth==0.5.3",
+3 -5
View File
@@ -25,11 +25,9 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
# Link existing accounts with the same email address
email = sociallogin.account.extra_data.get("email")
if sociallogin.provider.id == "saml":
# For SAML, the asserted NameID email cannot be trusted on its own:
# any tenant can claim any email domain in its SAML configuration. To
# prevent cross-tenant account takeover (GHSA-h8m9-jgf8-vwvp), only link
# the incoming SAML session to an existing account when (1) the email
# domain matches the tenant whose ACS endpoint is being used and (2) the
# For SAML, the asserted NameID email cannot be trusted on its own.
# Prevent cross-tenant account takeover (GHSA-h8m9-jgf8-vwvp) by
# linking only when the email domain matches the ACS endpoint and the
# existing user is already a member of that tenant.
email = sociallogin.user.email
if not email:
+9 -70
View File
@@ -1,13 +1,10 @@
from typing import Optional, Tuple
from math import isfinite
from uuid import UUID
from cryptography.fernet import InvalidToken
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from drf_simple_apikey.backends import APIKeyAuthentication as BaseAPIKeyAuth
from drf_simple_apikey.crypto import get_crypto
from drf_simple_apikey.settings import package_settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request
@@ -26,49 +23,18 @@ class TenantAPIKeyAuthentication(BaseAPIKeyAuth):
def _authenticate_credentials(self, request, key):
"""
Override to use admin connection, bypassing RLS during authentication.
Delegates to parent after temporarily routing model queries to admin DB.
"""
try:
payload = self.key_crypto.decrypt(key)
except ValueError:
raise AuthenticationFailed("Invalid API Key.")
if not isinstance(payload, dict):
raise AuthenticationFailed("Invalid API Key.")
payload_pk = payload.get("_pk")
payload_exp = payload.get("_exp")
if (
not isinstance(payload_pk, str)
or isinstance(payload_exp, bool)
or not isinstance(payload_exp, (int, float))
or not isfinite(payload_exp)
):
raise AuthenticationFailed("Invalid API Key.")
# Temporarily point the model's manager to admin database
original_objects = self.model.objects
self.model.objects = self.model.objects.using(MainRouter.admin_db)
try:
api_key_pk = UUID(payload_pk)
except ValueError:
raise AuthenticationFailed("Invalid API Key.")
if payload_exp < timezone.now().timestamp():
raise AuthenticationFailed("API Key has already expired.")
try:
api_key = self.model.objects.using(MainRouter.admin_db).get(id=api_key_pk)
except ObjectDoesNotExist:
raise AuthenticationFailed("No entity matching this api key.")
if api_key.revoked:
raise AuthenticationFailed("This API Key has been revoked.")
client_ip = request.META.get(package_settings.IP_ADDRESS_HEADER)
if api_key.blacklisted_ips and client_ip in api_key.blacklisted_ips:
raise AuthenticationFailed("Access denied from blacklisted IP.")
if api_key.whitelisted_ips and client_ip not in api_key.whitelisted_ips:
raise AuthenticationFailed("Access restricted to specific IP addresses.")
return api_key.entity, key
# Call parent method which will now use admin database
return super()._authenticate_credentials(request, key)
finally:
# Restore original manager
self.model.objects = original_objects
def authenticate(self, request: Request):
prefixed_key = self.get_key(request)
@@ -127,30 +93,3 @@ class CombinedJWTOrAPIKeyAuthentication(BaseAuthentication):
# Default fallback
return self.jwt_auth.authenticate(request)
class SSEAuthentication(CombinedJWTOrAPIKeyAuthentication):
"""JWT/API-Key auth that also accepts `?access_token=<jwt>`.
Browser `EventSource` is the only widely available SSE client API
and it cannot set the `Authorization` header (its constructor takes
only a URL and `withCredentials`). To keep browser SSE clients on
the same auth stack as the rest of the API, SSE endpoints additionally
accept a JWT via the `?access_token=<jwt>` query parameter — the
standard parameter name defined in RFC 6750 Section 2.3 for bearer tokens.
"""
def authenticate(self, request: Request):
auth_header = request.headers.get("Authorization", "")
if auth_header:
return super().authenticate(request)
raw_token = request.query_params.get("access_token")
if not raw_token:
# No header and no query token — let the default path raise
# the canonical AuthenticationFailed via the parent class.
return super().authenticate(request)
validated_token = self.jwt_auth.get_validated_token(raw_token)
user = self.jwt_auth.get_user(validated_token)
return user, validated_token
+2 -2
View File
@@ -112,14 +112,14 @@ def get_compliance_frameworks(provider_type: Provider.ProviderChoices) -> list[s
"""List compliance framework identifiers available for `provider_type`.
Includes both per-provider frameworks and universal top-level frameworks
(e.g. ``dora_2022_2554``, ``csa_ccm_4.0``).
(e.g. ``dora``, ``csa_ccm_4.0``).
Args:
provider_type (Provider.ProviderChoices): The cloud provider type
(e.g., "aws", "azure", "gcp", "m365").
Returns:
list[str]: Framework identifiers (e.g., "cis_1.4_aws", "dora_2022_2554").
list[str]: Framework identifiers (e.g., "cis_1.4_aws", "dora").
"""
global AVAILABLE_COMPLIANCE_FRAMEWORKS
if provider_type not in AVAILABLE_COMPLIANCE_FRAMEWORKS:
+5 -118
View File
@@ -102,7 +102,7 @@ class BaseProviderFilter(FilterSet):
"""
Abstract base filter for models with direct FK to Provider.
Provides standard provider_id, provider_type, and provider_groups filters.
Provides standard provider_id and provider_type filters.
Subclasses must define Meta.model.
"""
@@ -116,16 +116,6 @@ class BaseProviderFilter(FilterSet):
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
class Meta:
abstract = True
@@ -136,7 +126,7 @@ class BaseScanProviderFilter(FilterSet):
"""
Abstract base filter for models with FK to Scan (and Scan has FK to Provider).
Provides standard provider_id, provider_type, and provider_groups filters via scan relationship.
Provides standard provider_id and provider_type filters via scan relationship.
Subclasses must define Meta.model.
"""
@@ -150,16 +140,6 @@ class BaseScanProviderFilter(FilterSet):
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
provider_groups = UUIDFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
class Meta:
abstract = True
@@ -180,16 +160,6 @@ class CommonFindingFilters(FilterSet):
provider_type__in = ChoiceInFilter(
choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider"
)
provider_groups = UUIDFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
provider_uid = CharFilter(field_name="scan__provider__uid", lookup_expr="exact")
provider_uid__in = CharInFilter(field_name="scan__provider__uid", lookup_expr="in")
provider_uid__icontains = CharFilter(
@@ -400,12 +370,6 @@ class ProviderFilter(FilterSet):
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
provider_groups = UUIDFilter(
field_name="provider_groups__id", lookup_expr="exact", distinct=True
)
provider_groups__in = UUIDInFilter(
field_name="provider_groups__id", lookup_expr="in", distinct=True
)
class Meta:
model = Provider
@@ -431,16 +395,6 @@ class ProviderRelationshipFilterSet(FilterSet):
provider_type__in = ChoiceInFilter(
choices=Provider.ProviderChoices.choices, field_name="provider__provider"
)
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
provider_uid = CharFilter(field_name="provider__uid", lookup_expr="exact")
provider_uid__in = CharInFilter(field_name="provider__uid", lookup_expr="in")
provider_uid__icontains = CharFilter(
@@ -1047,16 +1001,6 @@ class FindingGroupSummaryFilter(_CheckTitleToCheckIdMixin, FilterSet):
field_name="provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = CharInFilter(field_name="provider__provider", lookup_expr="in")
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
class Meta:
model = FindingGroupDailySummary
@@ -1157,16 +1101,6 @@ class LatestFindingGroupSummaryFilter(_CheckTitleToCheckIdMixin, FilterSet):
field_name="provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = CharInFilter(field_name="provider__provider", lookup_expr="in")
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
class Meta:
model = FindingGroupDailySummary
@@ -1346,19 +1280,12 @@ class RoleFilter(FilterSet):
}
class ComplianceOverviewFilter(BaseScanProviderFilter):
"""
Keep provider filters in the schema while runtime filtering resolves scans first.
Compliance overview provider filters are applied to the latest completed scans
in the viewset, then this filterset handles the remaining compliance fields.
"""
class ComplianceOverviewFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
scan_id = UUIDFilter(field_name="scan_id")
scan_id = UUIDFilter(field_name="scan_id", required=True)
region = CharFilter(field_name="region")
class Meta(BaseScanProviderFilter.Meta):
class Meta:
model = ComplianceRequirementOverview
fields = {
"inserted_at": ["date", "gte", "lte"],
@@ -1379,16 +1306,6 @@ class ScanSummaryFilter(FilterSet):
provider_type__in = ChoiceInFilter(
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
)
provider_groups = UUIDFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
region = CharFilter(field_name="region")
class Meta:
@@ -1412,16 +1329,6 @@ class DailySeveritySummaryFilter(FilterSet):
provider_type__in = ChoiceInFilter(
field_name="provider__provider", choices=Provider.ProviderChoices.choices
)
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
date_from = DateFilter(method="filter_noop")
date_to = DateFilter(method="filter_noop")
@@ -1678,16 +1585,6 @@ class ThreatScoreSnapshotFilter(FilterSet):
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
provider_groups = UUIDFilter(
field_name="provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
compliance_id = CharFilter(field_name="compliance_id", lookup_expr="exact")
compliance_id__in = CharInFilter(field_name="compliance_id", lookup_expr="in")
@@ -1731,16 +1628,6 @@ class ResourceGroupOverviewFilter(FilterSet):
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
provider_groups = UUIDFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="exact",
distinct=True,
)
provider_groups__in = UUIDInFilter(
field_name="scan__provider__provider_groups__id",
lookup_expr="in",
distinct=True,
)
resource_group = CharFilter(field_name="resource_group", lookup_expr="exact")
resource_group__in = CharInFilter(field_name="resource_group", lookup_expr="in")
-26
View File
@@ -1,35 +1,9 @@
import logging
import time
from django.core.handlers.asgi import ASGIRequest
from django.db import connections
from config.custom_logging import BackendLogger
class CloseDBConnectionsMiddleware:
"""
Close request-scoped DB connections at the end of each ASGI request.
Under the ASGI worker, connections opened by sync views are not released
by Django's normal request-boundary cleanup, so they accumulate idle until
Postgres runs out of slots. Only ASGI requests are handled; the sync WSGI
test client manages its own connections and must be left alone.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
return self.get_response(request)
finally:
if isinstance(request, ASGIRequest):
for conn in connections.all(initialized_only=True):
if not conn.in_atomic_block:
conn.close_if_unusable_or_obsolete()
def extract_auth_info(request) -> dict:
if getattr(request, "auth", None) is not None:
tenant_id = request.auth.get("tenant_id", "N/A")
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.32.2
version: 1.31.3
description: |-
Prowler API specification.
-13
View File
@@ -1,13 +0,0 @@
"""Platform Server-Sent Events (SSE) infrastructure.
Wires `django-eventstream` into the API: a base viewset features
subclass to expose an SSE endpoint
(:class:`api.sse.base_views.BaseSSEViewSet`), the channel manager that
enforces the tenant gate (:class:`api.sse.channelmanager.SSEChannelManager`),
and the channel-name helpers (:func:`api.sse.utils.make_channel_name`).
"""
from api.sse.utils import make_channel_name
from api.sse.base_views import BaseSSEViewSet
__all__ = ["BaseSSEViewSet", "make_channel_name"]
-46
View File
@@ -1,46 +0,0 @@
"""Base view class for SSE endpoints."""
from api.authentication import SSEAuthentication
from api.base_views import BaseRLSViewSet
from django_eventstream.renderers import SSEEventRenderer
from django_eventstream.views import events
class BaseSSEViewSet(BaseRLSViewSet):
"""Base class for platform SSE endpoints.
Subclasses override method `get_channels` to declare the channel
names the connection should subscribe to the same way a regular
DRF viewset overrides method `get_queryset`. The channel manager
reads the result from `request.sse_channels`; there is no other
coupling between platform and feature.
"""
authentication_classes = [SSEAuthentication]
# Pin the SSE renderer so content negotiation accepts the browser's
# `Accept: text/event-stream`.
renderer_classes = [SSEEventRenderer]
def get_channels(self) -> set[str]:
"""Return the channels this connection subscribes to.
Implementations MUST raise the relevant DRF exceptions
(`NotAuthenticated`, `PermissionDenied`, `NotFound`) when
authorization fails. Returning an empty set would surface as
django-eventstream's "No channels specified" which masks the
real cause.
"""
raise NotImplementedError
def get_queryset(self):
# Most SSE viewsets only need `get_channels` and never call
# `get_queryset` (the SSE list path bypasses serialization
# entirely). Subclasses that perform their own queryset lookup
# inside `get_channels` should override; the default raises
# the same error a missing override on a ModelViewSet would.
raise NotImplementedError
def list(self, request, *_args, **kwargs):
"""Resolve channels under the regular DRF stack and stream."""
request.sse_channels = self.get_channels()
return events(request, **kwargs)
-75
View File
@@ -1,75 +0,0 @@
"""Channel manager that wires `django-eventstream` to platform SSE views."""
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from django_eventstream.channelmanager import DefaultChannelManager
from rest_framework.request import Request
from api.sse.utils import tenant_id_from_channel
if TYPE_CHECKING:
from api.models import User
class SSEChannelManager(DefaultChannelManager):
"""Connect `django-eventstream` to the platform's SSE viewsets."""
def get_channels_for_request(self, request: Request, view_kwargs: dict) -> set[str]: # noqa: vulture
"""Return the request's channels scoped to the active JWT tenant.
Args:
request: The authenticated DRF request, carrying `tenant_id`
(set by `BaseRLSViewSet`) and `sse_channels` (set by
`BaseSSEViewSet.list`).
view_kwargs: URL keyword arguments from django-eventstream;
unused because channels are resolved on the request.
Returns:
The subset of `request.sse_channels` whose embedded tenant
matches the active request tenant.
"""
try:
request_tenant_id = UUID(str(getattr(request, "tenant_id", None)))
except (TypeError, ValueError):
return set()
return {
channel
for channel in getattr(request, "sse_channels", set())
if tenant_id_from_channel(channel) == request_tenant_id
}
def can_read_channel(self, user: "User | None", channel: str) -> bool:
"""Re-verify tenant membership once the stream is established.
Args:
user: The connection's authenticated `User`, or `None` for an
anonymous connection django-eventstream passes `None`
rather than an `AnonymousUser`.
channel: The channel name being read, in the canonical
`<prefix>:<tenant_id>:<resource_id>` format.
Returns:
`True` only when `user` is authenticated and a member of the
tenant embedded in `channel`; `False` otherwise, including for
anonymous connections and malformed channel names.
"""
if user is None or not user.is_authenticated:
return False
tenant_id = tenant_id_from_channel(channel)
if tenant_id is None:
return False
return user.is_member_of_tenant(tenant_id)
def is_channel_reliable(self, channel: str) -> bool:
"""Report whether the channel keeps a server-side replay buffer.
Args:
channel: The channel name being queried.
Returns:
`False`, unconditionally. Replay storage is not configured
"""
return False
-51
View File
@@ -1,51 +0,0 @@
"""Channel-name convention shared by SSE publishers, consumers, and the
channel manager. The format is `<prefix>:<tenant_id>:<resource_id>`.
"""
from __future__ import annotations
import uuid
CHANNEL_SEPARATOR = ":"
def make_channel_name(
prefix: str,
tenant_id: str | uuid.UUID,
resource_id: str | uuid.UUID,
) -> str:
"""Build the canonical channel name for a resource.
Args:
prefix: Feature-owned prefix (e.g. `"lighthouse-session"`).
tenant_id: Tenant the resource belongs to.
resource_id: Resource identifier within the tenant.
Raises:
ValueError: If any segment contains `CHANNEL_SEPARATOR`, which
would break the `<prefix>:<tenant_id>:<resource_id>` contract
and let a crafted name smuggle extra segments past the parser.
"""
segments = (str(prefix), str(tenant_id), str(resource_id))
if any(CHANNEL_SEPARATOR in segment for segment in segments):
raise ValueError(
f"Channel segments must not contain '{CHANNEL_SEPARATOR}': {segments!r}"
)
return CHANNEL_SEPARATOR.join(segments)
def tenant_id_from_channel(channel: str) -> uuid.UUID | None:
"""Return the tenant UUID embedded in *channel*, or `None` if
*channel* does not follow the platform convention.
A `None` result MUST be treated by callers as "not authorized" or
a malformed channel cannot be safely read.
"""
segments = channel.split(CHANNEL_SEPARATOR)
if len(segments) != 3:
# Reject non-canonical names
return None
try:
return uuid.UUID(segments[1])
except ValueError:
return None
@@ -1,16 +1,13 @@
import time
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from uuid import uuid4
import pytest
from django.db.models.query import QuerySet
from django.test import RequestFactory
from rest_framework.exceptions import AuthenticationFailed
from api.authentication import SSEAuthentication, TenantAPIKeyAuthentication
from api.authentication import TenantAPIKeyAuthentication
from api.db_router import MainRouter
from api.models import TenantAPIKey
@@ -68,54 +65,6 @@ class TestTenantAPIKeyAuthentication:
# Verify the manager was restored
assert TenantAPIKey.objects == original_manager
def test_authenticate_credentials_keeps_manager_during_lookup(
self, auth_backend, api_keys_fixture, request_factory
):
"""Authentication must not expose a QuerySet as the model manager."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
_, encrypted_key = raw_key.split(TenantAPIKey.objects.separator, 1)
original_get = QuerySet.get
manager_has_create_api_key = []
def observe_manager(queryset, *args, **kwargs):
manager_has_create_api_key.append(
hasattr(TenantAPIKey.objects, "create_api_key")
)
return original_get(queryset, *args, **kwargs)
request = request_factory.get("/")
with patch.object(QuerySet, "get", observe_manager):
auth_backend._authenticate_credentials(request, encrypted_key)
assert manager_has_create_api_key
assert all(manager_has_create_api_key)
@pytest.mark.parametrize(
"payload",
[
{"_pk": str(uuid4()), "_exp": "not-a-timestamp"},
{
"_pk": "not-a-uuid",
"_exp": (datetime.now(timezone.utc) + timedelta(days=1)).timestamp(),
},
{"_pk": str(uuid4()), "_exp": True},
],
)
def test_authenticate_credentials_rejects_malformed_payloads(
self, auth_backend, request_factory, payload
):
"""Malformed decrypted payloads fail as authentication errors."""
request = request_factory.get("/")
encrypted_key = auth_backend.key_crypto.generate(payload)
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend._authenticate_credentials(request, encrypted_key)
assert str(exc_info.value.detail) == "Invalid API Key."
def test_authenticate_credentials_restores_manager_on_exception(
self, auth_backend, request_factory
):
@@ -433,62 +382,3 @@ class TestTenantAPIKeyAuthentication:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "API Key has already expired."
class TestSSEAuthentication:
"""`SSEAuthentication` adds an `?access_token=<jwt>` fallback for
browser `EventSource` clients while keeping the standard
`Authorization` header as the authoritative source."""
def test_header_present_delegates_to_super(self):
request = MagicMock()
request.headers = {"Authorization": "Bearer header-token"}
with patch.object(
SSEAuthentication.__bases__[0], "authenticate", return_value=("user", "tok")
) as super_auth:
result = SSEAuthentication().authenticate(request)
super_auth.assert_called_once_with(request)
assert result == ("user", "tok")
def test_no_header_no_query_token_delegates_to_super(self):
request = MagicMock()
request.headers = {}
request.query_params = {}
with patch.object(
SSEAuthentication.__bases__[0], "authenticate", return_value=None
) as super_auth:
result = SSEAuthentication().authenticate(request)
super_auth.assert_called_once_with(request)
assert result is None
def test_query_token_used_only_as_fallback(self):
request = MagicMock()
request.headers = {}
request.query_params = {"access_token": "query-jwt"}
jwt_instance = MagicMock()
jwt_instance.get_validated_token.return_value = "validated"
jwt_instance.get_user.return_value = "query-user"
with patch.object(SSEAuthentication, "jwt_auth", jwt_instance):
user, token = SSEAuthentication().authenticate(request)
jwt_instance.get_validated_token.assert_called_once_with("query-jwt")
assert user == "query-user"
assert token == "validated"
def test_query_token_invalid_raises_authentication_failed(self):
request = MagicMock()
request.headers = {}
request.query_params = {"access_token": "bad-token"}
jwt_instance = MagicMock()
jwt_instance.get_validated_token.side_effect = AuthenticationFailed(
"Invalid token"
)
with patch.object(SSEAuthentication, "jwt_auth", jwt_instance):
with pytest.raises(AuthenticationFailed):
SSEAuthentication().authenticate(request)
jwt_instance.get_validated_token.assert_called_once_with("bad-token")
@@ -41,30 +41,3 @@ class TestBuildCeleryBrokerUrl:
def test_invalid_scheme_raises_error(self):
with pytest.raises(ValueError, match="Invalid VALKEY_SCHEME 'http'"):
_build_celery_broker_url("http", "", "", "valkey", "6379", "0")
class TestCeleryWorkerConcurrency:
def _reimport_settings(self):
"""Fresh import — importlib.reload() doesn't clear the module namespace,
so an attribute set by a prior test would leak into the unset case."""
import sys
sys.modules.pop("config.settings.celery", None)
import config.settings.celery as celery_settings
return celery_settings
def test_unset_leaves_setting_absent(self, monkeypatch):
monkeypatch.delenv("DJANGO_CELERY_WORKER_CONCURRENCY", raising=False)
mod = self._reimport_settings()
assert not hasattr(mod, "CELERY_WORKER_CONCURRENCY")
def test_explicit_value_applied(self, monkeypatch):
monkeypatch.setenv("DJANGO_CELERY_WORKER_CONCURRENCY", "8")
mod = self._reimport_settings()
assert mod.CELERY_WORKER_CONCURRENCY == 8
def test_invalid_value_raises(self, monkeypatch):
monkeypatch.setenv("DJANGO_CELERY_WORKER_CONCURRENCY", "not-a-number")
with pytest.raises(ValueError):
self._reimport_settings()
-191
View File
@@ -1,191 +0,0 @@
"""Tests for the platform SSE infrastructure (``api.sse``).
Cover the two security-critical platform pieces the channel-name
convention (:mod:`api.sse.utils`) and the tenant gate enforced by
:class:`api.sse.channelmanager.SSEChannelManager`. The SSE authentication
class lives in :mod:`api.authentication` with the rest of the auth stack,
so its tests live in ``test_authentication.py``. Per-feature SSE endpoints
add their own tests on top of these.
"""
import uuid
from unittest.mock import MagicMock
import pytest
from django.http import StreamingHttpResponse
from rest_framework.test import APIRequestFactory, force_authenticate
from api.sse.base_views import BaseSSEViewSet
from api.sse.channelmanager import SSEChannelManager
from api.sse.utils import make_channel_name, tenant_id_from_channel
class TestMakeChannel:
def test_round_trips_tenant_id(self):
tenant_id = uuid.uuid4()
channel = make_channel_name("lighthouse-session", tenant_id, uuid.uuid4())
assert tenant_id_from_channel(channel) == tenant_id
def test_accepts_str_arguments(self):
tenant_id = uuid.uuid4()
channel = make_channel_name("lighthouse-session", str(tenant_id), "resource-1")
assert channel == f"lighthouse-session:{tenant_id}:resource-1"
def test_prefix_with_hyphen_is_not_split(self):
# Prefixes contain hyphens but never colons, so the tenant id is
# always the second colon-separated segment.
tenant_id = uuid.uuid4()
channel = make_channel_name("a-long-hyphenated-prefix", tenant_id, "res")
assert tenant_id_from_channel(channel) == tenant_id
@pytest.mark.parametrize(
"prefix, tenant_id, resource_id",
[
("evil:prefix", uuid.uuid4(), "res"),
("prefix", uuid.uuid4(), "res:extra"),
("prefix", "tenant:smuggled", "res"),
],
)
def test_rejects_separator_injection(self, prefix, tenant_id, resource_id):
# A colon in any segment would let a crafted name smuggle extra
# segments past the parser, so construction must fail loudly.
with pytest.raises(ValueError):
make_channel_name(prefix, tenant_id, resource_id)
class TestTenantIdFromChannel:
def test_returns_none_for_too_few_segments(self):
assert tenant_id_from_channel("prefix:only") is None
assert tenant_id_from_channel("garbage") is None
def test_returns_none_for_too_many_segments(self):
# A valid tenant UUID in position 1 must not authorize a
# non-canonical name that carries extra segments.
tenant_id = uuid.uuid4()
assert tenant_id_from_channel(f"prefix:{tenant_id}:resource:extra") is None
def test_returns_none_for_non_uuid_tenant_segment(self):
assert tenant_id_from_channel("prefix:not-a-uuid:resource") is None
def test_parses_valid_channel(self):
tenant_id = uuid.uuid4()
assert tenant_id_from_channel(f"prefix:{tenant_id}:resource") == tenant_id
@pytest.mark.django_db
class TestSSEChannelManager:
def test_member_can_read_own_tenant_channel(
self, create_test_user, tenants_fixture
):
tenant = tenants_fixture[0]
channel = make_channel_name("lighthouse-session", tenant.id, uuid.uuid4())
assert SSEChannelManager().can_read_channel(create_test_user, channel)
def test_non_member_cannot_read_other_tenant_channel(
self, create_test_user, tenants_fixture
):
# create_test_user is a member of tenant1 and tenant2 but not tenant3.
foreign_tenant = tenants_fixture[2]
channel = make_channel_name(
"lighthouse-session", foreign_tenant.id, uuid.uuid4()
)
assert not SSEChannelManager().can_read_channel(create_test_user, channel)
def test_anonymous_user_is_rejected(self, tenants_fixture):
channel = make_channel_name(
"lighthouse-session", tenants_fixture[0].id, uuid.uuid4()
)
assert not SSEChannelManager().can_read_channel(None, channel)
anon = MagicMock(is_authenticated=False)
assert not SSEChannelManager().can_read_channel(anon, channel)
def test_malformed_channel_is_rejected(self, create_test_user, tenants_fixture):
assert not SSEChannelManager().can_read_channel(create_test_user, "garbage")
def test_get_channels_for_request_returns_active_tenant_channels(self):
tenant_id = uuid.uuid4()
own = make_channel_name("prefix", tenant_id, "resource")
request = MagicMock()
request.tenant_id = str(tenant_id)
request.sse_channels = {own}
assert SSEChannelManager().get_channels_for_request(request, {}) == {own}
def test_get_channels_for_request_drops_other_tenant_channels(self):
# Fail-closed: a channel for a tenant other than the active JWT
# tenant is dropped before reaching django-eventstream, even if the
# viewset mistakenly stashed it. This is the primary tenant gate that
# binds authorization to request.tenant_id, not just membership.
active_tenant = uuid.uuid4()
own = make_channel_name("prefix", active_tenant, "resource")
foreign = make_channel_name("prefix", uuid.uuid4(), "resource")
request = MagicMock()
request.tenant_id = str(active_tenant)
request.sse_channels = {own, foreign}
assert SSEChannelManager().get_channels_for_request(request, {}) == {own}
def test_get_channels_for_request_drops_malformed_channels(self):
request = MagicMock()
request.tenant_id = str(uuid.uuid4())
request.sse_channels = {"garbage", "prefix:not-a-uuid:resource"}
assert SSEChannelManager().get_channels_for_request(request, {}) == set()
def test_get_channels_for_request_without_tenant_returns_empty(self):
# No active tenant on the request (auth/RLS never ran) → fail closed,
# regardless of any channels stashed on it.
request = MagicMock(spec=[])
assert SSEChannelManager().get_channels_for_request(request, {}) == set()
def test_get_channels_for_request_defaults_to_empty(self):
# A request that never went through BaseSSEViewSet.list has no
# sse_channels attribute; the manager must not raise.
request = object()
assert SSEChannelManager().get_channels_for_request(request, {}) == set()
def test_channel_is_not_reliable(self):
# v1 ships without server-side replay storage.
assert (
SSEChannelManager().is_channel_reliable("prefix:tenant:resource") is False
)
@pytest.mark.django_db
class TestBaseSSEViewSet:
"""End-to-end check that the base viewset opens a stream.
``BaseSSEViewSet.list`` hands the DRF ``Request`` straight to
django-eventstream's ``events()``, which is written for a plain
Django request. This drives a real request through the full DRF
stack (authentication, RLS, content negotiation, channel manager)
and asserts the result is an SSE stream, so the DRF/Django request
mismatch cannot regress silently.
"""
def test_list_opens_event_stream(self, create_test_user, tenants_fixture):
tenant = tenants_fixture[0]
channel = make_channel_name("test-sse", tenant.id, uuid.uuid4())
seen_tenant_ids = []
class _StreamingSSEViewSet(BaseSSEViewSet):
def get_channels(self):
# Reached only after dispatch/initial ran, so the RLS
# tenant context is already on the request.
seen_tenant_ids.append(self.request.tenant_id)
return {channel}
request = APIRequestFactory().get("/api/v1/test-sse/stream")
force_authenticate(
request, user=create_test_user, token={"tenant_id": str(tenant.id)}
)
view = _StreamingSSEViewSet.as_view({"get": "list"})
response = view(request)
# A StreamingHttpResponse (not the plain HttpResponse used for SSE
# error envelopes) means events() accepted the DRF request, the
# channel manager handed it a non-empty channel set, and the
# stream was opened end to end.
assert isinstance(response, StreamingHttpResponse)
assert response.status_code == 200
assert response["Content-Type"] == "text/event-stream"
assert seen_tenant_ids == [str(tenant.id)]
File diff suppressed because it is too large Load Diff
+1 -161
View File
@@ -1,10 +1,6 @@
import uuid
from django.http import QueryDict
from django.urls import reverse
from django_celery_results.models import TaskResult
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from api.exceptions import (
@@ -12,7 +8,7 @@ from api.exceptions import (
TaskInProgressException,
TaskNotFoundException,
)
from api.models import Provider, StateChoices, Task
from api.models import StateChoices, Task
from api.v1.serializers import TaskSerializer
@@ -78,162 +74,6 @@ class PaginateByPkMixin:
return self.get_paginated_response(serialized)
class JsonApiFilterMixin:
"""Shared helpers for manually applying django-filter to JSON:API params."""
jsonapi_filter_replace_dots = False
def _normalize_jsonapi_params(
self,
query_params,
exclude_keys=None,
replace_dots=None,
):
exclude_keys = exclude_keys or set()
if replace_dots is None:
replace_dots = self.jsonapi_filter_replace_dots
normalized = QueryDict(mutable=True)
for key, values in query_params.lists():
normalized_key = (
key[7:-1] if key.startswith("filter[") and key.endswith("]") else key
)
if replace_dots:
normalized_key = normalized_key.replace(".", "__")
if normalized_key not in exclude_keys:
normalized.setlist(normalized_key, values)
return normalized
def _apply_filterset(
self,
queryset,
filterset_class,
exclude_keys=None,
replace_dots=None,
):
normalized_params = self._normalize_jsonapi_params(
self.request.query_params,
exclude_keys=set(exclude_keys or []),
replace_dots=replace_dots,
)
filterset = filterset_class(normalized_params, queryset=queryset)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
return filterset.qs
class ProviderFilterParamsMixin(JsonApiFilterMixin):
"""Shared extraction of provider filters from JSON:API query params."""
PROVIDER_FILTER_KEYS = frozenset(
{
"provider_id",
"provider_id__in",
"provider_type",
"provider_type__in",
"provider_groups",
"provider_groups__in",
}
)
PROVIDER_FILTER_DOT_ALIAS_KEYS = frozenset(
{
"provider_id.in",
"provider_type.in",
"provider_groups.in",
}
)
PROVIDER_FILTER_QUERY_KEYS = PROVIDER_FILTER_KEYS | PROVIDER_FILTER_DOT_ALIAS_KEYS
def _csv_filter_values(self, value):
return [item.strip() for item in value.split(",") if item.strip()]
def _validate_uuid_filter_values(self, field_name, values):
try:
for value in values:
uuid.UUID(str(value))
except (TypeError, ValueError, AttributeError):
raise ValidationError({field_name: ["Enter a valid UUID."]})
def _has_provider_filters(self, include_dot_aliases=False):
provider_filter_keys = (
self.PROVIDER_FILTER_QUERY_KEYS
if include_dot_aliases
else self.PROVIDER_FILTER_KEYS
)
return any(
self.request.query_params.get(f"filter[{key}]")
for key in provider_filter_keys
)
def _extract_provider_filters_from_params(
self,
*,
validate_uuids=False,
include_dot_aliases=False,
):
params = self.request.query_params
filters = {}
valid_provider_types = {
choice[0] for choice in Provider.ProviderChoices.choices
}
provider_id = params.get("filter[provider_id]")
if provider_id:
if validate_uuids:
self._validate_uuid_filter_values("provider_id", [provider_id])
filters["provider_id"] = provider_id
provider_id_in = params.get("filter[provider_id__in]")
if include_dot_aliases:
provider_id_in = provider_id_in or params.get("filter[provider_id.in]")
if provider_id_in:
values = self._csv_filter_values(provider_id_in)
if validate_uuids:
self._validate_uuid_filter_values("provider_id__in", values)
filters["provider_id__in"] = values
provider_type = params.get("filter[provider_type]")
if provider_type:
if provider_type not in valid_provider_types:
raise ValidationError(
{"provider_type": f"Invalid choice: {provider_type}"}
)
filters["provider__provider"] = provider_type
provider_type_in = params.get("filter[provider_type__in]")
if include_dot_aliases:
provider_type_in = provider_type_in or params.get(
"filter[provider_type.in]"
)
if provider_type_in:
values = self._csv_filter_values(provider_type_in)
invalid = [value for value in values if value not in valid_provider_types]
if invalid:
raise ValidationError(
{"provider_type__in": f"Invalid choices: {', '.join(invalid)}"}
)
filters["provider__provider__in"] = values
provider_groups = params.get("filter[provider_groups]")
if provider_groups:
if validate_uuids:
self._validate_uuid_filter_values("provider_groups", [provider_groups])
filters["provider__provider_groups__id"] = provider_groups
provider_groups_in = params.get("filter[provider_groups__in]")
if include_dot_aliases:
provider_groups_in = provider_groups_in or params.get(
"filter[provider_groups.in]"
)
if provider_groups_in:
values = self._csv_filter_values(provider_groups_in)
if validate_uuids:
self._validate_uuid_filter_values("provider_groups__in", values)
filters["provider__provider_groups__id__in"] = values
return filters
class TaskManagementMixin:
"""
Mixin to manage task status checking.
+201 -297
View File
@@ -228,13 +228,7 @@ from api.utils import (
validate_invitation,
)
from api.uuid_utils import datetime_to_uuid7, uuid7_start
from api.v1.mixins import (
DisablePaginationMixin,
JsonApiFilterMixin,
PaginateByPkMixin,
ProviderFilterParamsMixin,
TaskManagementMixin,
)
from api.v1.mixins import DisablePaginationMixin, PaginateByPkMixin, TaskManagementMixin
from api.v1.serializers import (
AttackPathsCartographySchemaSerializer,
AttackPathsCustomQueryRunRequestSerializer,
@@ -812,70 +806,60 @@ class TenantFinishACSView(FinishACSView):
user.name = "N/A"
user.save()
# Only remap roles when the IdP provides a userType attribute.
# Without it, the user's current roles are left untouched.
role_name = (
extra.get("userType", [""])[0].strip() if extra.get("userType") else ""
extra.get("userType", ["no_permissions"])[0].strip()
if extra.get("userType")
else "no_permissions"
)
role = (
Role.objects.using(MainRouter.admin_db)
.filter(name=role_name, tenant=tenant)
.first()
)
if role_name:
with transaction.atomic(using=MainRouter.admin_db):
role = (
Role.objects.using(MainRouter.admin_db)
.filter(name=role_name, tenant=tenant)
.first()
)
# Only skip mapping if it would remove the last MANAGE_ACCOUNT user
remaining_manage_account_users = (
UserRoleRelationship.objects.using(MainRouter.admin_db)
.filter(role__manage_account=True, tenant_id=tenant.id)
.exclude(user_id=user_id)
.values("user")
.distinct()
.count()
)
user_has_manage_account = (
UserRoleRelationship.objects.using(MainRouter.admin_db)
.filter(
role__manage_account=True,
tenant_id=tenant.id,
user_id=user_id,
)
.exists()
)
role_manage_account = role.manage_account if role else False
would_remove_last_manage_account = (
user_has_manage_account
and remaining_manage_account_users == 0
and not role_manage_account
)
# Only skip mapping if it would remove the last MANAGE_ACCOUNT user
remaining_manage_account_users = (
UserRoleRelationship.objects.using(MainRouter.admin_db)
.filter(role__manage_account=True, tenant_id=tenant.id)
.exclude(user_id=user_id)
.values("user")
.distinct()
.count()
)
user_has_manage_account = (
UserRoleRelationship.objects.using(MainRouter.admin_db)
.filter(role__manage_account=True, tenant_id=tenant.id, user_id=user_id)
.exists()
)
role_manage_account = role.manage_account if role else False
would_remove_last_manage_account = (
user_has_manage_account
and remaining_manage_account_users == 0
and not role_manage_account
)
if not would_remove_last_manage_account:
if role is None:
# Roles auto-created from userType get read-only access:
# visibility over all providers, no management permissions
role, _ = Role.objects.using(MainRouter.admin_db).get_or_create(
name=role_name,
tenant=tenant,
defaults={
"manage_users": False,
"manage_account": False,
"manage_billing": False,
"manage_providers": False,
"manage_integrations": False,
"manage_scans": False,
"unlimited_visibility": True,
},
)
UserRoleRelationship.objects.using(MainRouter.admin_db).filter(
user=user,
tenant_id=tenant.id,
).delete()
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
user=user,
role=role,
tenant_id=tenant.id,
)
if not would_remove_last_manage_account:
if role is None:
role = Role.objects.using(MainRouter.admin_db).create(
name=role_name,
tenant=tenant,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
UserRoleRelationship.objects.using(MainRouter.admin_db).filter(
user=user,
tenant_id=tenant.id,
).delete()
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
user=user,
role=role,
tenant_id=tenant.id,
)
membership, _ = Membership.objects.using(MainRouter.admin_db).get_or_create(
user=user,
tenant=tenant,
@@ -1889,7 +1873,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
description=(
"Download a specific compliance report as an OCSF JSON file. "
"Only universal frameworks that declare an output configuration "
"produce this artifact (currently 'dora_2022_2554' and 'csa_ccm_4.0'); any "
"produce this artifact (currently 'dora' and 'csa_ccm_4.0'); any "
"other framework returns 404."
),
parameters=[
@@ -1898,7 +1882,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
type=str,
location=OpenApiParameter.PATH,
required=True,
description="The compliance report name, like 'dora_2022_2554'",
description="The compliance report name, like 'dora'",
),
],
responses={
@@ -4567,19 +4551,15 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
@extend_schema_view(
list=extend_schema(
tags=["Compliance Overview"],
summary="List compliance overviews",
description=(
"Retrieve compliance overview data for a scan. When provider filters "
"are provided, the endpoint uses the latest completed scan for each "
"matching provider."
),
summary="List compliance overviews for a scan",
description="Retrieve an overview of all the compliance in a given scan.",
parameters=[
OpenApiParameter(
name="filter[scan_id]",
required=False,
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Related scan ID. Required unless a provider filter is provided.",
description="Related scan ID.",
),
],
responses={
@@ -4594,23 +4574,19 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
description="Compliance overviews generation task failed"
),
},
filters=True,
),
metadata=extend_schema(
tags=["Compliance Overview"],
summary="Retrieve metadata values from compliance overviews",
description=(
"Fetch unique metadata values from compliance overviews. This is useful "
"for dynamic filtering. When provider filters are provided, metadata is "
"computed from the latest completed scan for each matching provider."
),
description="Fetch unique metadata values from a set of compliance overviews. This is useful for dynamic "
"filtering.",
parameters=[
OpenApiParameter(
name="filter[scan_id]",
required=False,
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Related scan ID. Required unless a provider filter is provided.",
description="Related scan ID.",
),
],
responses={
@@ -4625,24 +4601,19 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
description="Compliance overviews generation task failed"
),
},
filters=True,
),
requirements=extend_schema(
tags=["Compliance Overview"],
summary="List compliance requirements overview",
description=(
"Retrieve a detailed overview of compliance requirements, grouped by "
"compliance framework. This endpoint provides requirement-level details "
"and aggregates status across regions. When provider filters are provided, "
"the endpoint uses the latest completed scan for each matching provider."
),
summary="List compliance requirements overview for a scan",
description="Retrieve a detailed overview of compliance requirements in a given scan, grouped by compliance "
"framework. This endpoint provides requirement-level details and aggregates status across regions.",
parameters=[
OpenApiParameter(
name="filter[scan_id]",
required=False,
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Related scan ID. Required unless a provider filter is provided.",
description="Related scan ID.",
),
OpenApiParameter(
name="filter[compliance_id]",
@@ -4701,10 +4672,7 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="requirements")
@method_decorator(CACHE_DECORATOR, name="attributes")
class ComplianceOverviewViewSet(
ProviderFilterParamsMixin, BaseRLSViewSet, TaskManagementMixin
):
jsonapi_filter_replace_dots = True
class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
pagination_class = ComplianceOverviewPagination
queryset = ComplianceRequirementOverview.objects.all()
serializer_class = ComplianceOverviewSerializer
@@ -4718,22 +4686,28 @@ class ComplianceOverviewViewSet(
required_permissions = []
def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return ComplianceRequirementOverview.objects.none()
role = get_role(self.request.user, self.request.tenant_id)
unlimited_visibility = getattr(
role, Permissions.UNLIMITED_VISIBILITY.value, False
)
base_queryset = ComplianceRequirementOverview.objects.filter(
tenant_id=self.request.tenant_id
)
if unlimited_visibility:
return base_queryset
base_queryset = self.filter_queryset(
ComplianceRequirementOverview.objects.filter(
tenant_id=self.request.tenant_id
)
)
else:
providers = Provider.objects.filter(
provider_groups__in=role.provider_groups.all()
).distinct()
base_queryset = self.filter_queryset(
ComplianceRequirementOverview.objects.filter(
tenant_id=self.request.tenant_id, scan__provider__in=providers
)
)
return base_queryset.filter(scan__provider__in=get_providers(role))
return base_queryset
def get_serializer_class(self):
if hasattr(self, "response_serializer_class"):
@@ -4771,72 +4745,6 @@ class ComplianceOverviewViewSet(
return summaries
def _validate_scan_selection(self, scan_id, has_provider_filters):
if scan_id and has_provider_filters:
raise ValidationError(
[
{
"detail": "Use either filter[scan_id] or provider filters.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "invalid",
}
]
)
if scan_id:
self._validate_uuid_filter_values("scan_id", [scan_id])
return
if has_provider_filters:
return
raise ValidationError(
[
{
"detail": "This query parameter is required unless a provider filter is provided.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)
def _latest_scan_ids_for_provider_filters(self):
role = get_role(self.request.user, self.request.tenant_id)
scans = Scan.all_objects.filter(
tenant_id=self.request.tenant_id,
state=StateChoices.COMPLETED,
)
if not getattr(role, Permissions.UNLIMITED_VISIBILITY.value, False):
scans = scans.filter(provider__in=get_providers(role))
provider_filters = self._extract_provider_filters_from_params(
validate_uuids=True,
include_dot_aliases=True,
)
if provider_filters:
scans = scans.filter(**provider_filters)
return list(
scans.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
def _filtered_queryset_for_latest_provider_scans(self, latest_scan_ids=None):
if latest_scan_ids is None:
latest_scan_ids = self._latest_scan_ids_for_provider_filters()
queryset = self.get_queryset().filter(scan_id__in=latest_scan_ids)
# Provider filters stay on the filterset for OpenAPI docs, but runtime
# filtering happens on Scan first so compliance queries use scan IDs.
return self._apply_filterset(
queryset,
self.filterset_class,
exclude_keys=self.PROVIDER_FILTER_KEYS | {"scan_id"},
)
def _get_compliance_template(self, *, provider=None, scan_id=None):
"""Return the compliance template for the given provider or scan."""
if provider is None and scan_id is not None:
@@ -4952,36 +4860,6 @@ class ComplianceOverviewViewSet(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def _task_response_for_latest_provider_scans(self, latest_scan_ids):
for scan_id in latest_scan_ids:
task_response = self._task_response_if_running(str(scan_id))
if task_response:
return task_response
return None
def _latest_provider_scan_ids_without_data(self, latest_scan_ids):
data_presence_queryset = self.get_queryset().filter(scan_id__in=latest_scan_ids)
scan_ids_with_data = {
str(scan_id)
for scan_id in data_presence_queryset.values_list(
"scan_id", flat=True
).distinct()
}
return [
scan_id
for scan_id in latest_scan_ids
if str(scan_id) not in scan_ids_with_data
]
def _task_response_for_latest_provider_scans_without_data(
self,
latest_scan_ids,
):
scan_ids_to_check = self._latest_provider_scan_ids_without_data(
latest_scan_ids,
)
return self._task_response_for_latest_provider_scans(scan_ids_to_check)
def _list_with_region_filter(self, scan_id, region_filter):
"""
Fall back to detailed ComplianceRequirementOverview query when region filter is applied.
@@ -5022,25 +4900,8 @@ class ComplianceOverviewViewSet(
return Response(data)
def _list_with_latest_provider_filters(self):
latest_scan_ids = self._latest_scan_ids_for_provider_filters()
queryset = self._filtered_queryset_for_latest_provider_scans(latest_scan_ids)
data = self._aggregate_compliance_overview(queryset)
task_response = self._task_response_for_latest_provider_scans_without_data(
latest_scan_ids,
)
if task_response:
return task_response
return Response(data)
def list(self, request, *args, **kwargs):
scan_id = request.query_params.get("filter[scan_id]")
has_provider_filters = self._has_provider_filters(include_dot_aliases=True)
self._validate_scan_selection(scan_id, has_provider_filters)
if has_provider_filters:
return self._list_with_latest_provider_filters()
# Specific scan requested - use optimized summaries with region support
region_filter = request.query_params.get(
@@ -5086,34 +4947,33 @@ class ComplianceOverviewViewSet(
@action(detail=False, methods=["get"], url_name="metadata")
def metadata(self, request):
scan_id = request.query_params.get("filter[scan_id]")
has_provider_filters = self._has_provider_filters(include_dot_aliases=True)
self._validate_scan_selection(scan_id, has_provider_filters)
latest_scan_ids = None
if has_provider_filters:
latest_scan_ids = self._latest_scan_ids_for_provider_filters()
queryset = self._filtered_queryset_for_latest_provider_scans(
latest_scan_ids
if not scan_id:
raise ValidationError(
[
{
"detail": "This query parameter is required.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)
else:
queryset = self._apply_filterset(self.get_queryset(), self.filterset_class)
regions = list(
queryset.values_list("region", flat=True).order_by("region").distinct()
self.get_queryset()
.filter(scan_id=scan_id)
.values_list("region", flat=True)
.order_by("region")
.distinct()
)
result = {"regions": regions}
task_response = None
if has_provider_filters:
task_response = self._task_response_for_latest_provider_scans_without_data(
latest_scan_ids,
)
elif not regions:
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
if regions:
serializer = self.get_serializer(data=result)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
if has_provider_filters and task_response:
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
serializer = self.get_serializer(data=result)
@@ -5123,10 +4983,19 @@ class ComplianceOverviewViewSet(
@action(detail=False, methods=["get"], url_name="requirements")
def requirements(self, request):
scan_id = request.query_params.get("filter[scan_id]")
has_provider_filters = self._has_provider_filters(include_dot_aliases=True)
compliance_id = request.query_params.get("filter[compliance_id]")
self._validate_scan_selection(scan_id, has_provider_filters)
if not scan_id:
raise ValidationError(
[
{
"detail": "This query parameter is required.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)
if not compliance_id:
raise ValidationError(
@@ -5139,16 +5008,7 @@ class ComplianceOverviewViewSet(
}
]
)
latest_scan_ids = None
if has_provider_filters:
latest_scan_ids = self._latest_scan_ids_for_provider_filters()
filtered_queryset = self._filtered_queryset_for_latest_provider_scans(
latest_scan_ids
)
else:
filtered_queryset = self._apply_filterset(
self.get_queryset(), self.filterset_class
)
filtered_queryset = self.filter_queryset(self.get_queryset())
all_requirements = filtered_queryset.values(
"requirement_id",
@@ -5207,22 +5067,13 @@ class ComplianceOverviewViewSet(
requirements_summary, many=True
)
task_response = None
if has_provider_filters:
task_response = self._task_response_for_latest_provider_scans_without_data(
latest_scan_ids,
)
elif not requirements_summary:
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
if has_provider_filters and task_response:
return task_response
if requirements_summary:
return Response(serializer.data, status=status.HTTP_200_OK)
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"], url_name="attributes")
@@ -5461,7 +5312,7 @@ class ComplianceOverviewViewSet(
),
)
@method_decorator(CACHE_DECORATOR, name="list")
class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
class OverviewViewSet(BaseRLSViewSet):
queryset = ScanSummary.objects.all()
http_method_names = ["get"]
ordering = ["-inserted_at"]
@@ -5578,6 +5429,18 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
tenant_id=tenant_id, scan_id__in=latest_scan_ids
)
def _normalize_jsonapi_params(self, query_params, exclude_keys=None):
"""Convert JSON:API filter params (filter[X]) to flat params (X)."""
exclude_keys = exclude_keys or set()
normalized = QueryDict(mutable=True)
for key, values in query_params.lists():
normalized_key = (
key[7:-1] if key.startswith("filter[") and key.endswith("]") else key
)
if normalized_key not in exclude_keys:
normalized.setlist(normalized_key, values)
return normalized
def _ensure_allowed_providers(self):
"""Populate allowed providers for RBAC-aware queries once per request."""
if getattr(self, "_providers_initialized", False):
@@ -5597,6 +5460,15 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
return queryset.filter(**provider_filter)
return queryset
def _apply_filterset(self, queryset, filterset_class, exclude_keys=None):
normalized_params = self._normalize_jsonapi_params(
self.request.query_params, exclude_keys=set(exclude_keys or [])
)
filterset = filterset_class(normalized_params, queryset=queryset)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
return filterset.qs
def _latest_scan_ids_for_allowed_providers(self, tenant_id, provider_filters=None):
provider_filter = self._get_provider_filter()
queryset = Scan.all_objects.filter(
@@ -5610,6 +5482,40 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
.values_list("id", flat=True)
)
def _extract_provider_filters_from_params(self):
"""Extract and validate provider filters from query params."""
params = self.request.query_params
filters = {}
valid_provider_types = {c[0] for c in Provider.ProviderChoices.choices}
provider_id = params.get("filter[provider_id]")
if provider_id:
filters["provider_id"] = provider_id
provider_id_in = params.get("filter[provider_id__in]")
if provider_id_in:
filters["provider_id__in"] = provider_id_in.split(",")
provider_type = params.get("filter[provider_type]")
if provider_type:
if provider_type not in valid_provider_types:
raise ValidationError(
{"provider_type": f"Invalid choice: {provider_type}"}
)
filters["provider__provider"] = provider_type
provider_type_in = params.get("filter[provider_type__in]")
if provider_type_in:
types = provider_type_in.split(",")
invalid = [t for t in types if t not in valid_provider_types]
if invalid:
raise ValidationError(
{"provider_type__in": f"Invalid choices: {', '.join(invalid)}"}
)
filters["provider__provider__in"] = types
return filters
@action(detail=False, methods=["get"], url_name="providers")
def providers(self, request):
tenant_id = self.request.tenant_id
@@ -5680,11 +5586,15 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
tenant_id = self.request.tenant_id
providers_qs = Provider.objects.filter(tenant_id=tenant_id)
self._ensure_allowed_providers()
if hasattr(self, "allowed_providers"):
providers_qs = providers_qs.filter(
id__in=self.allowed_providers.values("id")
)
allowed_ids = list(self.allowed_providers.values_list("id", flat=True))
if not allowed_ids:
overview = []
return Response(
self.get_serializer(overview, many=True).data,
status=status.HTTP_200_OK,
)
providers_qs = providers_qs.filter(id__in=allowed_ids)
overview = (
providers_qs.values("provider")
@@ -5900,41 +5810,29 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
description="Retrieve a specific snapshot by ID. If not provided, returns latest snapshots.",
),
OpenApiParameter(
name="filter[provider_id]",
name="provider_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter by specific provider ID",
),
OpenApiParameter(
name="filter[provider_id__in]",
name="provider_id__in",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider IDs (comma-separated UUIDs)",
),
OpenApiParameter(
name="filter[provider_type]",
name="provider_type",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by provider type (aws, azure, gcp, etc.)",
),
OpenApiParameter(
name="filter[provider_type__in]",
name="provider_type__in",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider types (comma-separated)",
),
OpenApiParameter(
name="filter[provider_groups]",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter by provider group ID",
),
OpenApiParameter(
name="filter[provider_groups__in]",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider group IDs (comma-separated UUIDs)",
),
],
)
@action(detail=False, methods=["get"], url_name="threatscore")
@@ -6276,8 +6174,6 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
"provider_id__in",
"provider_type",
"provider_type__in",
"provider_groups",
"provider_groups__in",
}
filtered_queryset = self._apply_filterset(
base_queryset, CategoryOverviewFilter, exclude_keys=provider_filter_keys
@@ -6347,8 +6243,6 @@ class OverviewViewSet(ProviderFilterParamsMixin, BaseRLSViewSet):
"provider_id__in",
"provider_type",
"provider_type__in",
"provider_groups",
"provider_groups__in",
}
filtered_queryset = self._apply_filterset(
base_queryset,
@@ -7399,7 +7293,7 @@ SEVERITY_ORDER_REVERSE = {v: k for k, v in SEVERITY_ORDER.items()}
),
retrieve=extend_schema(exclude=True),
)
class FindingGroupViewSet(JsonApiFilterMixin, BaseRLSViewSet):
class FindingGroupViewSet(BaseRLSViewSet):
"""
ViewSet for Finding Groups - aggregates findings by check_id.
@@ -7415,7 +7309,6 @@ class FindingGroupViewSet(JsonApiFilterMixin, BaseRLSViewSet):
queryset = FindingGroupDailySummary.objects.all()
serializer_class = FindingGroupSerializer
filterset_class = FindingGroupFilter
jsonapi_filter_replace_dots = True
filter_backends = [
jsonapi_filters.QueryParameterValidationFilter,
jsonapi_filters.OrderingFilter,
@@ -7466,6 +7359,18 @@ class FindingGroupViewSet(JsonApiFilterMixin, BaseRLSViewSet):
return queryset
def _normalize_jsonapi_params(self, query_params):
"""Convert JSON:API filter params (filter[X]) to flat params (X)."""
normalized = QueryDict(mutable=True)
for key, values in query_params.lists():
normalized_key = (
key[7:-1] if key.startswith("filter[") and key.endswith("]") else key
)
# Convert JSON:API dot notation to Django double underscore
normalized_key = normalized_key.replace(".", "__")
normalized.setlist(normalized_key, values)
return normalized
@extend_schema(exclude=True)
def retrieve(self, request, *args, **kwargs):
raise MethodNotAllowed(method="GET")
@@ -8584,10 +8489,9 @@ class FindingGroupViewSet(JsonApiFilterMixin, BaseRLSViewSet):
This endpoint returns finding groups without requiring date filters,
automatically using the latest available data per check_id.
Provider, provider group, check, and computed filters are still supported.
All other filters (provider_id, provider_type, check_id) are still supported.
""",
tags=["Finding Groups"],
filters=True,
)
@action(detail=False, methods=["get"], url_name="latest")
def latest(self, request):
-4
View File
@@ -3,7 +3,6 @@ from datetime import timedelta
from config.custom_logging import LOGGING # noqa
from config.env import BASE_DIR, env # noqa
from config.settings.celery import * # noqa
from config.settings.eventstream import * # noqa
from config.settings.partitions import * # noqa
from config.settings.sentry import * # noqa
from config.settings.social_login import * # noqa
@@ -45,11 +44,9 @@ INSTALLED_APPS = [
"dj_rest_auth.registration",
"rest_framework.authtoken",
"drf_simple_apikey",
"django_eventstream",
]
MIDDLEWARE = [
"api.middleware.CloseDBConnectionsMiddleware",
"django_guid.middleware.guid_middleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@@ -139,7 +136,6 @@ SPECTACULAR_SETTINGS = {
}
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
DJANGO_GUID = {
"GUID_HEADER_NAME": "Transaction-ID",
-28
View File
@@ -3,8 +3,6 @@ import multiprocessing
import os
import threading
from uvicorn_worker import UvicornWorker
from config.env import env
# Ensure the environment variable for Django settings is set
@@ -14,7 +12,6 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.production")
import django # noqa: E402
django.setup()
from api.compliance import warm_compliance_caches # noqa: E402
from config.django.production import LOGGING as DJANGO_LOGGERS, DEBUG # noqa: E402
from config.custom_logging import BackendLogger # noqa: E402
@@ -22,37 +19,12 @@ from config.custom_logging import BackendLogger # noqa: E402
BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1")
PORT = env("DJANGO_PORT", default=8080)
class ProwlerUvicornWorker(UvicornWorker):
CONFIG_KWARGS = {
# Keep-alive idle timeout. Must exceed the load balancer idle timeout.
"timeout_keep_alive": env.int("GUNICORN_KEEPALIVE", default=75),
"loop": "uvloop",
"lifespan": "off", # Django ASGIHandler doesn't handle lifespan scopes
}
# Required so SSE endpoints can keep the event loop alive while waiting for events
worker_class = env(
"DJANGO_WORKER_CLASS",
default="config.guniconf.ProwlerUvicornWorker",
)
# Server settings
bind = f"{BIND_ADDRESS}:{PORT}"
workers = env.int("DJANGO_WORKERS", default=multiprocessing.cpu_count() * 2 + 1)
reload = DEBUG
# Preload the application before forking workers in production: the app is
# imported once in the master and workers fork from it. In development, disable
# preload so the server restarts on code changes.
preload_app = not DEBUG
# Worker timeout in seconds. Increased from the default 30s to handle requests
# that may take longer, such as complex API operations.
timeout = env.int("GUNICORN_TIMEOUT", default=120)
# Logging
logconfig_dict = DJANGO_LOGGERS
gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)
@@ -53,8 +53,3 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
CELERY_DEADLOCK_ATTEMPTS = env.int("DJANGO_CELERY_DEADLOCK_ATTEMPTS", default=5)
# Opt-in override for Celery's prefork pool size. When unset, Celery falls back
# to its default (os.cpu_count()).
if "DJANGO_CELERY_WORKER_CONCURRENCY" in env.ENVIRON:
CELERY_WORKER_CONCURRENCY = env.int("DJANGO_CELERY_WORKER_CONCURRENCY")
@@ -1,41 +0,0 @@
"""Server-Sent Events (SSE) configuration.
Wires django-eventstream into the platform: Valkey Pub/Sub backend on a
dedicated DB (separate from the Celery broker), the platform channel
manager, and headers that match the existing CORS allowlist.
"""
from config.env import env
from config.settings.celery import (
VALKEY_HOST,
VALKEY_PASSWORD,
VALKEY_PORT,
VALKEY_SCHEME,
VALKEY_USERNAME,
)
# Dedicated Valkey DB for the SSE Pub/Sub bus. Kept distinct from the
# Celery broker DB so a noisy broker can't shoulder out streaming
# traffic on the same keyspace.
EVENTSTREAM_VALKEY_DB = env.int("EVENTSTREAM_VALKEY_DB", default=2)
EVENTSTREAM_REDIS: dict = {
"host": VALKEY_HOST,
"port": int(VALKEY_PORT),
"db": EVENTSTREAM_VALKEY_DB,
}
if VALKEY_PASSWORD:
EVENTSTREAM_REDIS["password"] = VALKEY_PASSWORD
if VALKEY_USERNAME:
EVENTSTREAM_REDIS["username"] = VALKEY_USERNAME
if VALKEY_SCHEME == "rediss":
EVENTSTREAM_REDIS["ssl"] = True
# Platform channel manager — performs the per-feature authorization and
# rewrites the placeholder channel from the URL into the canonical
# tenant-scoped channel name. See ``api.sse.channelmanager``.
EVENTSTREAM_CHANNELMANAGER_CLASS = "api.sse.channelmanager.SSEChannelManager"
# Headers a browser EventSource may legitimately send. Keep tight; the
# stream itself reads no body, so no permissive defaults.
EVENTSTREAM_ALLOW_HEADERS = "Cache-Control, Last-Event-ID"
@@ -76,8 +76,6 @@ IGNORED_EXCEPTIONS = [
# PowerShell Errors in User Authentication
"Microsoft Teams User Auth connection failed: Please check your permissions and try again.",
"Exchange Online User Auth connection failed: Please check your permissions and try again.",
# ASGI: Client disconnected before the response finished (health-check probes on /health/live)
"RequestAborted",
]
+19 -55
View File
@@ -14,7 +14,7 @@ from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
from config.env import env
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
from django.db import DatabaseError, IntegrityError, OperationalError, transaction
from django.db import IntegrityError, OperationalError
from django.db.models import (
Case,
Count,
@@ -43,7 +43,7 @@ from api.db_utils import (
psycopg_connection,
rls_transaction,
)
from api.exceptions import ProviderConnectionError, ProviderDeletedException
from api.exceptions import ProviderConnectionError
from api.models import (
AttackSurfaceOverview,
ComplianceOverviewSummary,
@@ -118,20 +118,6 @@ ATTACK_SURFACE_PROVIDER_COMPATIBILITY = {
_ATTACK_SURFACE_MAPPING_CACHE: dict[str, dict] = {}
def _save_scan_instance(
scan_instance: Scan, provider_id: str, update_fields: list[str]
) -> None:
try:
with transaction.atomic(): # Savepoint for not killing the `rls_transaction`
scan_instance.save(update_fields=update_fields)
except DatabaseError:
if Scan.objects.filter(pk=scan_instance.id).exists():
raise
raise ProviderDeletedException(
f"Provider '{provider_id}' for scan '{scan_instance.id}' was deleted during the scan"
) from None
def aggregate_category_counts(
categories: list[str],
severity: str,
@@ -1044,18 +1030,13 @@ def perform_prowler_scan(
group_resources_cache: dict[str, set] = {}
start_time = time.time()
exc = None
skip_final_scan_update = False
with rls_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
scan_instance = Scan.objects.get(pk=scan_id)
scan_instance.state = StateChoices.EXECUTING
scan_instance.started_at = datetime.now(tz=timezone.utc)
_save_scan_instance(
scan_instance,
provider_id,
["state", "started_at", "updated_at"],
)
scan_instance.save(update_fields=["state", "started_at", "updated_at"])
# Find the mutelist processor if it exists
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
@@ -1123,7 +1104,7 @@ def perform_prowler_scan(
# Throttle scan_instance progress writes to avoid hammering the writer:
# only persist when progress moves by at least `PROGRESS_THROTTLE_DELTA`
# OR `PROGRESS_THROTTLE_SECONDS` have elapsed. The final progress (100)
# OR `PROGRESS_THROTTLE_SECONDS` have elapsed. The final progress (1.0)
# always persists in the `finally` block below.
last_persisted_progress = -1.0
last_persisted_progress_at = 0.0
@@ -1165,11 +1146,7 @@ def perform_prowler_scan(
):
with rls_transaction(tenant_id):
scan_instance.progress = progress
_save_scan_instance(
scan_instance,
provider_id,
["progress", "updated_at"],
)
scan_instance.save(update_fields=["progress", "updated_at"])
last_persisted_progress = progress
last_persisted_progress_at = now
@@ -1196,39 +1173,26 @@ def perform_prowler_scan(
batch_size=SCAN_DB_BATCH_SIZE,
)
except ProviderDeletedException as e:
logger.warning(str(e))
exception = e
skip_final_scan_update = True
except Exception as e:
logger.error(f"Error performing scan {scan_id}: {e}")
exception = e
scan_instance.state = StateChoices.FAILED
finally:
if not skip_final_scan_update:
try:
with rls_transaction(tenant_id):
scan_instance.duration = time.time() - start_time
scan_instance.completed_at = datetime.now(tz=timezone.utc)
scan_instance.unique_resource_count = len(unique_resources)
if exception is None:
scan_instance.progress = 100
_save_scan_instance(
scan_instance,
provider_id,
[
"state",
"duration",
"completed_at",
"unique_resource_count",
"progress",
"updated_at",
],
)
except ProviderDeletedException as e:
logger.warning(str(e))
exception = e
with rls_transaction(tenant_id):
scan_instance.duration = time.time() - start_time
scan_instance.completed_at = datetime.now(tz=timezone.utc)
scan_instance.unique_resource_count = len(unique_resources)
scan_instance.save(
update_fields=[
"state",
"duration",
"completed_at",
"unique_resource_count",
"progress",
"updated_at",
]
)
if exception is not None:
raise exception
+2 -2
View File
@@ -560,7 +560,7 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
# Per-framework exporters in `COMPLIANCE_CLASS_MAP` consume the legacy bulk.
frameworks_bulk = Compliance.get_bulk(provider_type)
# Universal-only frameworks (top-level JSONs like `dora_2022_2554.json`) are emitted
# Universal-only frameworks (top-level JSONs like `dora.json`) are emitted
# via `process_universal_compliance_frameworks` below.
universal_bulk = get_prowler_provider_compliance(provider_type)
universal_only_names = {
@@ -650,7 +650,7 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
writer.batch_write_data_to_file(**extra)
writer._data.clear()
# Universal-only frameworks (e.g. `dora_2022_2554.json`).
# Universal-only frameworks (e.g. `dora.json`).
if universal_only_names:
process_universal_compliance_frameworks(
input_compliance_frameworks=universal_only_names,
+14 -84
View File
@@ -8,19 +8,6 @@ from io import StringIO
from unittest.mock import MagicMock, patch
import pytest
from api.db_router import MainRouter
from api.exceptions import ProviderConnectionError, ProviderDeletedException
from api.models import (
Finding,
MuteRule,
Provider,
Resource,
ResourceScanSummary,
Scan,
ScanSummary,
StateChoices,
StatusChoices,
)
from tasks.jobs.scan import (
_ATTACK_SURFACE_MAPPING_CACHE,
_aggregate_findings_by_region,
@@ -42,6 +29,19 @@ from tasks.jobs.scan import (
)
from tasks.utils import CustomEncoder
from api.db_router import MainRouter
from api.exceptions import ProviderConnectionError
from api.models import (
Finding,
MuteRule,
Provider,
Resource,
ResourceScanSummary,
Scan,
ScanSummary,
StateChoices,
StatusChoices,
)
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
@@ -263,75 +263,6 @@ class TestPerformScan:
assert provider.connected is False
assert isinstance(provider.connection_last_checked_at, datetime)
def test_perform_prowler_scan_provider_deleted_during_progress_update(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
tenant_id = str(tenant.id)
scan_id = str(scan.id)
provider_id = str(provider.id)
def scan_results():
Provider.objects.filter(pk=provider_id).delete()
yield 50, []
with (
patch(
"tasks.jobs.scan.initialize_prowler_provider",
return_value=MagicMock(),
),
patch("tasks.jobs.scan.ProwlerScan") as mock_prowler_scan_class,
patch("tasks.jobs.scan.logger.error") as mock_logger_error,
):
mock_prowler_scan_instance = MagicMock()
mock_prowler_scan_instance.scan.return_value = scan_results()
mock_prowler_scan_class.return_value = mock_prowler_scan_instance
with pytest.raises(ProviderDeletedException):
perform_prowler_scan(tenant_id, scan_id, provider_id, [])
mock_logger_error.assert_not_called()
assert not Scan.objects.filter(pk=scan_id).exists()
def test_perform_prowler_scan_sets_final_progress_when_progress_updates_are_throttled(
self,
tenants_fixture,
scans_fixture,
providers_fixture,
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
provider = providers_fixture[0]
tenant_id = str(tenant.id)
scan_id = str(scan.id)
provider_id = str(provider.id)
with (
patch(
"tasks.jobs.scan.initialize_prowler_provider",
return_value=MagicMock(),
),
patch("tasks.jobs.scan.ProwlerScan") as mock_prowler_scan_class,
patch("tasks.jobs.scan.PROGRESS_THROTTLE_DELTA", 200),
patch("tasks.jobs.scan.PROGRESS_THROTTLE_SECONDS", 3600),
):
mock_prowler_scan_instance = MagicMock()
mock_prowler_scan_instance.scan.return_value = [(99, []), (100, [])]
mock_prowler_scan_class.return_value = mock_prowler_scan_instance
perform_prowler_scan(tenant_id, scan_id, provider_id, [])
scan.refresh_from_db()
assert scan.state == StateChoices.COMPLETED
assert scan.progress == 100
@pytest.mark.parametrize(
"last_status, new_status, expected_delta",
[
@@ -4660,9 +4591,8 @@ class TestScanIsFullScope:
# If the SDK adds a new filter, this test still passes via the
# introspection-driven derivation; if it adds a non-filter kwarg
# (e.g. provider-like), keep the exclusion list in sync in models.py.
import inspect
from prowler.lib.scan.scan import Scan as ProwlerScan
import inspect
expected = tuple(
name
Generated
+93 -220
View File
@@ -16,7 +16,7 @@ constraints = [
{ name = "aiobotocore", specifier = "==2.25.1" },
{ name = "aiofiles", specifier = "==24.1.0" },
{ name = "aiohappyeyeballs", specifier = "==2.6.1" },
{ name = "aiohttp", specifier = "==3.14.0" },
{ name = "aiohttp", specifier = "==3.13.5" },
{ name = "aioitertools", specifier = "==0.13.0" },
{ name = "aiosignal", specifier = "==1.4.0" },
{ name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" },
@@ -61,8 +61,9 @@ constraints = [
{ name = "astroid", specifier = "==3.2.4" },
{ name = "async-timeout", specifier = "==5.0.1" },
{ name = "attrs", specifier = "==25.4.0" },
{ name = "authlib", specifier = "==1.6.12" },
{ name = "authlib", specifier = "==1.6.9" },
{ name = "autopep8", specifier = "==2.3.2" },
{ name = "awsipranges", specifier = "==0.3.3" },
{ name = "azure-cli-core", specifier = "==2.83.0" },
{ name = "azure-cli-telemetry", specifier = "==1.1.0" },
{ name = "azure-common", specifier = "==1.1.28" },
@@ -145,7 +146,6 @@ constraints = [
{ name = "django-celery-results", specifier = "==2.6.0" },
{ name = "django-cors-headers", specifier = "==4.4.0" },
{ name = "django-environ", specifier = "==0.11.2" },
{ name = "django-eventstream", specifier = "==5.3.3" },
{ name = "django-filter", specifier = "==24.3" },
{ name = "django-guid", specifier = "==3.5.0" },
{ name = "django-postgres-extra", specifier = "==2.0.9" },
@@ -190,7 +190,7 @@ constraints = [
{ name = "grpc-google-iam-v1", specifier = "==0.14.3" },
{ name = "grpcio", specifier = "==1.76.0" },
{ name = "grpcio-status", specifier = "==1.76.0" },
{ name = "gunicorn", specifier = "==26.0.0" },
{ name = "gunicorn", specifier = "==23.0.0" },
{ name = "h11", specifier = "==0.16.0" },
{ name = "h2", specifier = "==4.3.0" },
{ name = "hpack", specifier = "==4.1.0" },
@@ -199,8 +199,8 @@ constraints = [
{ name = "httpx", specifier = "==0.28.1" },
{ name = "humanfriendly", specifier = "==10.0" },
{ name = "hyperframe", specifier = "==6.1.0" },
{ name = "iamdata", specifier = "==0.1.202605131" },
{ name = "idna", specifier = "==3.15" },
{ name = "iamdata", specifier = "==0.1.202602021" },
{ name = "idna", specifier = "==3.11" },
{ name = "importlib-metadata", specifier = "==8.7.1" },
{ name = "inflection", specifier = "==0.5.1" },
{ name = "iniconfig", specifier = "==2.3.0" },
@@ -252,7 +252,7 @@ constraints = [
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "nest-asyncio", specifier = "==1.6.0" },
{ name = "nltk", specifier = "==3.9.4" },
{ name = "numpy", specifier = "==2.2.6" },
{ name = "numpy", specifier = "==2.0.2" },
{ name = "oauthlib", specifier = "==3.3.1" },
{ name = "oci", specifier = "==2.169.0" },
{ name = "openai", specifier = "==1.109.1" },
@@ -281,7 +281,7 @@ constraints = [
{ name = "psutil", specifier = "==7.2.2" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "py-deviceid", specifier = "==0.1.1" },
{ name = "py-iam-expand", specifier = "==0.3.0" },
{ name = "py-iam-expand", specifier = "==0.1.0" },
{ name = "py-ocsf-models", specifier = "==0.8.1" },
{ name = "pyasn1", specifier = "==0.6.3" },
{ name = "pyasn1-modules", specifier = "==0.4.2" },
@@ -357,8 +357,6 @@ constraints = [
{ name = "uritemplate", specifier = "==4.2.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "uuid6", specifier = "==2024.7.10" },
{ name = "uvicorn", specifier = "==0.49.0" },
{ name = "uvloop", specifier = "==0.22.1" },
{ name = "vine", specifier = "==5.1.0" },
{ name = "vulture", specifier = "==2.14" },
{ name = "wcwidth", specifier = "==0.5.3" },
@@ -471,7 +469,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.14.0"
version = "3.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -480,47 +478,44 @@ dependencies = [
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "typing-extensions" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" },
{ url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" },
{ url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" },
{ url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" },
{ url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" },
{ url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" },
{ url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" },
{ url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" },
{ url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" },
{ url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" },
{ url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" },
{ url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" },
{ url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" },
{ url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" },
{ url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" },
{ url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" },
{ url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" },
{ url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" },
{ url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" },
{ url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" },
{ url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" },
{ url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" },
{ url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" },
{ url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" },
{ url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" },
{ url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" },
{ url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" },
{ url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" },
{ url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" },
{ url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" },
{ url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" },
{ url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" },
{ url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" },
{ url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" },
{ url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" },
{ url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" },
{ url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" },
{ url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" },
{ url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
]
[[package]]
@@ -1050,6 +1045,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" },
]
[[package]]
name = "awsipranges"
version = "0.3.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/2e/6efa95f995369da828715f41705686cd214b9259ed758266942553d40441/awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0", size = 16739, upload-time = "2022-02-10T21:08:32.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/ce/5c9a8bf91bdc9592a409c99e58fd99f2727ab8d634719c0ad796021b76d7/awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf", size = 18106, upload-time = "2022-02-10T21:08:31.497Z" },
]
[[package]]
name = "azure-cli-core"
version = "2.83.0"
@@ -2223,18 +2227,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
name = "deprecated"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
name = "detect-secrets"
version = "1.5.0"
@@ -2370,19 +2362,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/f1/468b49cccba3b42dda571063a14c668bb0b53a1d5712426d18e36663bd53/django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05", size = 19141, upload-time = "2023-09-01T21:02:59.88Z" },
]
[[package]]
name = "django-eventstream"
version = "5.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-grip" },
{ name = "gripcontrol" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/49/ec6cbc24f3f30465370df7096cfea9722bad2b0c1f35a7ff5d45fb96cff6/django_eventstream-5.3.3.tar.gz", hash = "sha256:6880b03298eebf18c1b736b972fb862eaf631dfbb79f8b27496418a3495d08dc", size = 47622, upload-time = "2025-10-23T00:22:40.291Z" }
[[package]]
name = "django-filter"
version = "24.3"
@@ -2395,19 +2374,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/09/b1/92f1c30b47c1ebf510c35a2ccad9448f73437e5891bbd2b4febe357cc3de/django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", size = 95011, upload-time = "2024-08-02T13:27:55.616Z" },
]
[[package]]
name = "django-grip"
version = "3.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "gripcontrol" },
{ name = "pubcontrol" },
{ name = "six" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/2c7b04fa864073cd8cb324f8674672162282d97540d56732cbd3a9ae5bca/django-grip-3.5.2.tar.gz", hash = "sha256:1ee1601492cd110256bd03e4a68797a9fbefa27c15f5a838bf245df97db0450c", size = 7626, upload-time = "2025-03-24T18:53:58.677Z" }
[[package]]
name = "django-guid"
version = "3.5.0"
@@ -3019,17 +2985,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" },
]
[[package]]
name = "gripcontrol"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pubcontrol" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4f/51/1cbf88384fbe97a1454fb0adddcdca8cb90ceb99c3250274c334db844f4f/gripcontrol-4.4.0.tar.gz", hash = "sha256:44ee6fe244a02870aa4e5bc810138ccaf5070dce5eb149b8ee9e27b960a95c2d", size = 12526, upload-time = "2026-05-14T21:19:28.49Z" }
[[package]]
name = "grpc-google-iam-v1"
version = "0.14.3"
@@ -3091,14 +3046,14 @@ wheels = [
[[package]]
name = "gunicorn"
version = "26.0.0"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
@@ -3200,20 +3155,20 @@ wheels = [
[[package]]
name = "iamdata"
version = "0.1.202605131"
version = "0.1.202602021"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/ea/d68e25aa4392e8a9f8e6523adc95a5fb86baf98d052efa2cec4d4a00e7ce/iamdata-0.1.202605131.tar.gz", hash = "sha256:ab4e8f1ea080394157848fecd0ca643633e35b2e0cb1965c9ed9bdd673afe00c", size = 793465, upload-time = "2026-05-13T05:57:10.607Z" }
sdist = { url = "https://files.pythonhosted.org/packages/93/5e/8179963b7a528c548824a8e4088150509d9fa8571dd622b7399f6d2d5680/iamdata-0.1.202602021.tar.gz", hash = "sha256:c24265fc3694076f65da91a8aa9361b60da25f7b8cfd8ba4ddd6aa1b9bb5153e", size = 771233, upload-time = "2026-02-02T05:49:56.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/93/03396b477b0faa9f1a12142209b59aa13d0fe4f64e2be47883f607789c14/iamdata-0.1.202605131-py3-none-any.whl", hash = "sha256:350e317d96fb8c8ddf30aa6da222788d302af5f13c9e357b59f9eefe50b8af5a", size = 1259166, upload-time = "2026-05-13T05:57:09.093Z" },
{ url = "https://files.pythonhosted.org/packages/74/9e/ae7a3019aa5a27d70412b74da4f0304695efa5d9a88f0689f37ea2602ea2/iamdata-0.1.202602021-py3-none-any.whl", hash = "sha256:48419662d75dd0e1ea22b9cc98fd70201d4c72760c6897acc46ad9ab90633d18", size = 1226614, upload-time = "2026-02-02T05:49:54.735Z" },
]
[[package]]
name = "idna"
version = "3.15"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
@@ -3525,20 +3480,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070, upload-time = "2025-02-18T21:06:31.391Z" },
]
[[package]]
name = "linode-api4"
version = "5.45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "polling" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/b5/fce03d9b81008dcc0fe4961ce10e140ac3ae5ab17f2cdd659763e4964c0d/linode_api4-5.45.0.tar.gz", hash = "sha256:af8a0a5638345ad467447112dcf5d58ec47e7dd192b89ce0c8537a1e5c435d04", size = 283375, upload-time = "2026-06-11T18:05:13.671Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/38/19e3c8f7b7a9dbeea2aa5af61f70162bff5131b3d39acbe73e8d0dd12972/linode_api4-5.45.0-py3-none-any.whl", hash = "sha256:3cc2650b13d8d3bc7735fa8e92a639669618f320471dc8e519db778c6020eacd", size = 158336, upload-time = "2026-06-11T18:05:11.799Z" },
]
[[package]]
name = "lxml"
version = "6.1.0"
@@ -4030,30 +3971,30 @@ wheels = [
[[package]]
name = "numpy"
version = "2.2.6"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" },
{ url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" },
{ url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" },
{ url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" },
{ url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" },
{ url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" },
{ url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" },
{ url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" },
{ url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" },
{ url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" },
{ url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" },
{ url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" },
{ url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" },
{ url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" },
{ url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" },
]
[[package]]
@@ -4358,12 +4299,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/f5/65b66420c275e9b26513fdd6d84687403d11ac8be4650b67d1e5572b8f48/policyuniverse-1.5.1.20231109-py2.py3-none-any.whl", hash = "sha256:0b0ece0ee8285af31fc39ce09c82a551ca62e62bc2842e23952503bccb973321", size = 484251, upload-time = "2023-11-30T19:12:43.463Z" },
]
[[package]]
name = "polling"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/c5/4249317962180d97ec7a60fe38aa91f86216533bd478a427a5468945c5c9/polling-0.3.2.tar.gz", hash = "sha256:3afd62320c99b725c70f379964bf548b302fc7f04d4604e6c315d9012309cc9a", size = 5189, upload-time = "2021-05-22T19:48:41.466Z" }
[[package]]
name = "portalocker"
version = "2.10.1"
@@ -4480,8 +4415,8 @@ wheels = [
[[package]]
name = "prowler"
version = "5.31.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.31#fb995a79bfd318e0ae5e6d2653c039c103bda9b5" }
version = "5.30.0"
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=v5.30#f1d741214a60df17158c3fdc97804fd1fde64f3a" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4497,6 +4432,7 @@ dependencies = [
{ name = "alibabacloud-tea-openapi" },
{ name = "alibabacloud-vpc20160428" },
{ name = "alive-progress" },
{ name = "awsipranges" },
{ name = "azure-identity" },
{ name = "azure-keyvault-keys" },
{ name = "azure-mgmt-apimanagement" },
@@ -4539,7 +4475,6 @@ dependencies = [
{ name = "h2" },
{ name = "jsonschema" },
{ name = "kubernetes" },
{ name = "linode-api4" },
{ name = "markdown" },
{ name = "microsoft-kiota-abstractions" },
{ name = "msgraph-sdk" },
@@ -4569,7 +4504,7 @@ dependencies = [
[[package]]
name = "prowler-api"
version = "1.32.2"
version = "1.31.3"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -4582,7 +4517,6 @@ dependencies = [
{ name = "django-celery-results" },
{ name = "django-cors-headers" },
{ name = "django-environ" },
{ name = "django-eventstream" },
{ name = "django-filter" },
{ name = "django-guid" },
{ name = "django-postgres-extra" },
@@ -4609,8 +4543,6 @@ dependencies = [
{ name = "sentry-sdk", extra = ["django"] },
{ name = "sqlparse" },
{ name = "uuid6" },
{ name = "uvicorn-worker" },
{ name = "uvloop" },
{ name = "werkzeug" },
{ name = "xmlsec" },
]
@@ -4649,7 +4581,6 @@ requires-dist = [
{ name = "django-celery-results", specifier = "==2.6.0" },
{ name = "django-cors-headers", specifier = "==4.4.0" },
{ name = "django-environ", specifier = "==0.11.2" },
{ name = "django-eventstream", specifier = "==5.3.3" },
{ name = "django-filter", specifier = "==24.3" },
{ name = "django-guid", specifier = "==3.5.0" },
{ name = "django-postgres-extra", specifier = "==2.0.9" },
@@ -4662,22 +4593,20 @@ requires-dist = [
{ name = "drf-spectacular-jsonapi", specifier = "==0.5.1" },
{ name = "fonttools", specifier = "==4.62.1" },
{ name = "gevent", specifier = "==25.9.1" },
{ name = "gunicorn", specifier = "==26.0.0" },
{ name = "gunicorn", specifier = "==23.0.0" },
{ name = "h2", specifier = "==4.3.0" },
{ name = "lxml", specifier = "==6.1.0" },
{ name = "markdown", specifier = "==3.10.2" },
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "neo4j", specifier = "==6.1.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.31" },
{ name = "prowler", git = "https://github.com/prowler-cloud/prowler.git?rev=v5.30" },
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pytest-celery", extras = ["redis"], specifier = "==1.3.0" },
{ name = "reportlab", specifier = "==4.4.10" },
{ name = "sentry-sdk", extras = ["django"], specifier = "==2.56.0" },
{ name = "sqlparse", specifier = "==0.5.5" },
{ name = "uuid6", specifier = "==2024.7.10" },
{ name = "uvicorn-worker", specifier = "==0.4.0" },
{ name = "uvloop", specifier = "==0.22.1" },
{ name = "werkzeug", specifier = "==3.1.7" },
{ name = "xmlsec", specifier = "==1.3.17" },
]
@@ -4752,16 +4681,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", size = 1163864, upload-time = "2023-10-28T09:37:28.155Z" },
]
[[package]]
name = "pubcontrol"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyjwt", extra = ["crypto"] },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/6a/02202a247214a6ffd5148ab1b16aca1c334b40dca211bca0442c8b7c7447/pubcontrol-3.5.0.tar.gz", hash = "sha256:a5ec6b3f53edfd005675518e5e4cc23b34122776835ae7c6dbd1db173d1ff0cb", size = 18199, upload-time = "2023-07-05T19:11:40.477Z" }
[[package]]
name = "py-deviceid"
version = "0.1.1"
@@ -4773,14 +4692,14 @@ wheels = [
[[package]]
name = "py-iam-expand"
version = "0.3.0"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "iamdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/08/f6e11a029b81f0bec4b7b1f18704aadf509a882cc386c90ef1ac043c18cc/py_iam_expand-0.3.0.tar.gz", hash = "sha256:4ccfe25f40ba0633a152c4f86b49cde8972ee3d4b6009b017a4310cc4b9e64c7", size = 10234, upload-time = "2026-02-24T09:47:47.772Z" }
sdist = { url = "https://files.pythonhosted.org/packages/22/99/8d31a30b37825577275bb3663885b55075fba80257fcd6813b85d3aaffa8/py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96", size = 10228, upload-time = "2025-04-30T07:15:35.304Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/dd/4056d0bc3d6317039d2dd2ee7cd6a5389575603e270399a8f9f20e11e721/py_iam_expand-0.3.0-py3-none-any.whl", hash = "sha256:94c0a1e9dd60316ce60ddc0cdc9a046119bde335b5bb9593ee29224857860d5a", size = 12527, upload-time = "2026-02-24T09:47:45.602Z" },
{ url = "https://files.pythonhosted.org/packages/79/19/482c2e0768cda7afaed07918e4fbd951e2418255fb5d1d9b35b284871716/py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510", size = 12522, upload-time = "2025-04-30T07:15:33.799Z" },
]
[[package]]
@@ -5882,52 +5801,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/3e/4ae6af487ce5781ed71d5fe10aca72e7cbc4d4f45afc31b120287082a8dd/uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7", size = 6376, upload-time = "2024-07-10T16:39:36.148Z" },
]
[[package]]
name = "uvicorn"
version = "0.49.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
]
[[package]]
name = "uvicorn-worker"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gunicorn" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/59/9101b9c0680fd80e9d26c07deb822a5d18a324339fcf9cd017885ee808ad/uvicorn_worker-0.4.0.tar.gz", hash = "sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493", size = 9361, upload-time = "2025-09-20T10:47:01.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/25/09cd7a90c8bb7fb693be0d6704fccd5f9778d5513214b7a01cc4a94ff314/uvicorn_worker-0.4.0-py3-none-any.whl", hash = "sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde", size = 5364, upload-time = "2025-09-20T10:46:59.776Z" },
]
[[package]]
name = "uvloop"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
]
[[package]]
name = "vine"
version = "5.1.0"
@@ -1,7 +1,7 @@
# Build command
# docker build --platform=linux/amd64 --no-cache -t prowler:latest .
ARG PROWLER_VERSION=latest@sha256:4b796c6df40a3350c7947747b59bdda230d0da6222287500e13b0a8e1574aad4
ARG PROWLER_VERSION=latest
FROM toniblyx/prowler:${PROWLER_VERSION}
@@ -16,6 +16,7 @@ from typing import Optional
from prowler.lib.logger import logger
from lib.models import ConnectivityGraph
# ---------------------------------------------------------------------------
# JSON output
# ---------------------------------------------------------------------------
+1 -29
View File
@@ -28,7 +28,7 @@ containers:
image:
repository: prowlercloud/prowler-api
pullPolicy: IfNotPresent
command: ["/home/prowler/docker-entrypoint.sh", "beat"]
command: ["../docker-entrypoint.sh", "beat"]
secrets:
POSTGRES_HOST:
@@ -438,34 +438,6 @@ mainConfig:
# Minimum number of Availability Zones that an ELBv2 must be in
elbv2_min_azs: 2
# AWS Post-Quantum TLS Configuration
# aws.acmpca_certificate_authority_pqc_key_algorithm
acmpca_pqc_key_algorithms:
- "ML_DSA_44"
- "ML_DSA_65"
- "ML_DSA_87"
# aws.cloudfront_distributions_pqc_tls_enabled
cloudfront_pqc_min_protocol_versions:
- "TLSv1.3_2025"
# aws.apigateway_domain_name_pqc_tls_enabled
apigateway_pqc_tls_allowed_policies:
- "SecurityPolicy_TLS13_1_2_FIPS_PFS_PQ_2025_09"
- "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09"
- "SecurityPolicy_TLS13_1_2_PQ_2025_09"
# AWS Post-Quantum SSH Key Exchange Configuration
# aws.transfer_server_pqc_ssh_kex_enabled
transfer_pqc_ssh_allowed_policies:
- "TransferSecurityPolicy-2025-03"
- "TransferSecurityPolicy-FIPS-2025-03"
- "TransferSecurityPolicy-AS2Restricted-2025-07"
# aws.rolesanywhere_trust_anchor_pqc_pki
rolesanywhere_pqc_pca_key_algorithms:
- "ML_DSA_44"
- "ML_DSA_65"
- "ML_DSA_87"
# AWS Secrets Configuration
# Patterns to ignore in the secrets checks
@@ -11,7 +11,8 @@ data:
{{- else }}
AUTH_URL: {{ .Values.ui.authUrl | quote }}
{{- end }}
UI_API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1"
UI_API_DOCS_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1/docs"
API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1"
NEXT_PUBLIC_API_BASE_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1"
NEXT_PUBLIC_API_DOCS_URL: "http://{{ include "prowler.fullname" . }}-api:{{ .Values.api.service.port }}/api/v1/docs"
AUTH_TRUST_HOST: "true"
UI_PORT: {{ .Values.ui.service.port | quote }}
+1 -1
View File
@@ -440,7 +440,7 @@ worker_beat:
tag: ""
command:
- /home/prowler/docker-entrypoint.sh
- ../docker-entrypoint.sh
args:
- beat
+2 -2
View File
@@ -21,8 +21,8 @@ fullnameOverride: ""
secrets:
SITE_URL: http://localhost:3000
UI_API_BASE_URL: http://prowler-api:8080/api/v1
UI_API_DOCS_URL: http://prowler-api:8080/api/v1/docs
API_BASE_URL: http://prowler-api:8080/api/v1
NEXT_PUBLIC_API_DOCS_URL: http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST: True
UI_PORT: 3000
# openssl rand -base64 32
@@ -16,7 +16,7 @@
services:
nginx:
image: nginx:alpine@sha256:8b1e78743a03dbb2c95171cc58639fef29abc8816598e27fb910ed2e621e589a
image: nginx:alpine
container_name: prowler-nginx
restart: unless-stopped
ports:
+5 -5
View File
@@ -1,6 +1,6 @@
services:
api-dev-init:
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
image: busybox:1.37.0
volumes:
- ./_data/api:/data
command: ["sh", "-c", "chown -R 1000:1000 /data"]
@@ -64,7 +64,7 @@ services:
condition: service_healthy
postgres:
image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
image: postgres:16.3-alpine3.20
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -88,7 +88,7 @@ services:
retries: 5
valkey:
image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
image: valkey/valkey:7-alpine3.19
hostname: "valkey"
volumes:
- ./_data/valkey:/data
@@ -104,7 +104,7 @@ services:
retries: 3
neo4j:
image: graphstack/dozerdb:5.26.3.0@sha256:a77526ea3918fdc46d1fff70c4aea7d71d3874a26ecec059179d6775845b1247
image: graphstack/dozerdb:5.26.3.0
hostname: "neo4j"
volumes:
- ./_data/neo4j:/data
@@ -185,7 +185,7 @@ services:
soft: 65536
hard: 65536
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "../docker-entrypoint.sh"
- "beat"
mcp-server:
+5 -5
View File
@@ -6,7 +6,7 @@
#
services:
api-init:
image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028
image: busybox:1.37.0
volumes:
- ./_data/api:/data
command: ["sh", "-c", "chown -R 1000:1000 /data"]
@@ -60,7 +60,7 @@ services:
start_period: 60s
postgres:
image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
image: postgres:16.3-alpine3.20
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -80,7 +80,7 @@ services:
retries: 5
valkey:
image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
image: valkey/valkey:7-alpine3.19
hostname: "valkey"
volumes:
- ./_data/valkey:/data
@@ -96,7 +96,7 @@ services:
retries: 3
neo4j:
image: graphstack/dozerdb:5.26.3.0@sha256:a77526ea3918fdc46d1fff70c4aea7d71d3874a26ecec059179d6775845b1247
image: graphstack/dozerdb:5.26.3.0
hostname: "neo4j"
volumes:
- ./_data/neo4j:/data
@@ -160,7 +160,7 @@ services:
soft: 65536
hard: 65536
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "../docker-entrypoint.sh"
- "beat"
mcp-server:
@@ -40,181 +40,9 @@ When adding a new configurable check to Prowler, update the following files:
# aws.awslambda_function_vpc_multi_az
lambda_min_azs: 2
```
- **Provider Schema:** Add the typed field to the provider's Pydantic schema in `prowler/config/schema/<provider>.py`. This is required: the loader validates user configs against these schemas and the shipped `config.yaml` must round-trip with zero warnings. See [Adding a Parameter to the Provider Schema](#adding-a-parameter-to-the-provider-schema) below.
- **Test Fixtures:** If tests depend on this configuration, add the variable to `tests/config/fixtures/config.yaml`.
- **Documentation:** Document the new variable in the list of configurable checks in `docs/tutorials/configuration_file.md`.
For a complete list of checks that already support configuration, see the [Configuration File Tutorial](/user-guide/cli/tutorials/configuration_file).
## Adding a Parameter to the Provider Schema
Most providers have a typed Pydantic schema in `prowler/config/schema/`, registered in `prowler/config/schema/registry.py`. When a config is loaded and the provider has a registered schema, `validate_provider_config` checks each user-supplied key against it, logs a warning, and drops any field that fails validation. The consumer's `.get(key, default)` then falls back to the built-in default. Providers without a registered schema are passed through unchanged.
This catches typos in a value (for example, `0.2` typed as `20`, or `"medium"` for an enum that expects `"MEDIUM"`). It does NOT catch typos in a key name: `disalowed_regions` (one `l` missing) is treated as an unknown key and passes through untouched, because third-party check plugins legitimately rely on unknown keys being preserved. Reviewers should still check that any new key the YAML adds is named exactly the same as the field on the schema.
### Where to Add the Field
1. Open `prowler/config/schema/<provider>.py` (for example, `aws.py`).
2. Add a field on the provider's schema class. Always make it `Optional[...] = None` so the absence of the key is valid.
3. Apply the tightest type the value allows. Examples below.
If you are introducing an entirely new provider rather than a new parameter, also add an entry mapping the provider name to its schema class in `prowler/config/schema/registry.py`. The loader uses that registry to find the schema for the provider it is loading.
### Choosing the Right Type
| Value kind | Field declaration |
|---|---|
| Boolean toggle | `Optional[bool] = None` |
| Strictly positive integer (days, counts) | `Optional[int] = Field(default=None, gt=0)` |
| Fraction in 0..1 (threshold) | `Optional[float] = Field(default=None, ge=0.0, le=1.0)` |
| Closed set of strings | `Optional[Literal["A", "B", "C"]] = None` |
| Free-form string | `Optional[str] = None` |
| List of strings or ints | `Optional[list[str]] = None` |
Prefer `Literal[...]` over `str` whenever the value is one of a known set. Prefer `Field(gt=0)` over `int` whenever zero or negative would be nonsensical. The point of the schema is to catch real-world mistakes that previously passed silently.
### Custom Validators (Only When Needed)
If the value has structural rules beyond type and range, add a `field_validator`. Examples already in `aws.py`:
- `_validate_port_range` rejects ports outside `0..65535`.
- `_validate_account_ids` rejects anything that isn't a 12-digit AWS account ID.
- `_validate_trusted_ips` rejects entries that aren't a valid IP or CIDR.
Raise `ValueError` from the validator. The framework converts the error into a warning and drops the offending key.
### Example: Adding a New Parameter
Say a new check needs `max_iam_role_session_hours`, a strictly positive integer that defaults to 12 in code.
1. **Schema** (`prowler/config/schema/aws.py`):
```python
# IAM
max_iam_role_session_hours: Optional[int] = Field(default=None, gt=0)
```
2. **Shipped config** (`prowler/config/config.yaml`):
```yaml
# aws.iam_role_session_duration_within_limit
max_iam_role_session_hours: 12
```
3. **Consumer** (the check):
```python
max_hours = iam_client.audit_config.get("max_iam_role_session_hours", 12)
```
4. **Tests** in `tests/config/schema/aws_schema_test.py`:
- one test for a valid value that round-trips,
- one test for an invalid value (zero, negative, wrong type) that is dropped.
### What the Loader Guarantees
- **Unknown keys pass through.** Third-party check plugins can introduce arbitrary keys without schema edits; they will not be filtered.
- **Invalid values never crash the run.** They produce a single warning per field and the key is dropped.
- **Coerced values are normalized.** A YAML-quoted `"180"` for an `int` field arrives downstream as the integer `180`.
- **The shipped `config.yaml` must round-trip cleanly.** The integration test `test_shipped_default_config_loads_without_warnings` will fail if a key is added to the YAML without a matching schema field, so the two stay in sync.
## Configuration Value Limits
Configurable thresholds enforce hard limits. A value outside the documented range is **dropped with a warning** and the check falls back to its built-in default (the same as if the key were absent). These bounds are intentionally conservative: they are not the absolute service maxima but the range that still produces a meaningful security check.
Use this section as the reference when upgrading an existing config: if a value you set is being rejected, it is outside the range below.
Only fields with a numeric range, a fixed value set, or a length cap are listed. Fields typed as free-form strings or lists (for example `disallowed_regions`, `secrets_ignore_patterns`, `trusted_account_ids`) have no range limit — they are validated for shape only (a 12-digit account ID, a valid IP/CIDR, a dotted version string), not for magnitude.
### AWS
| Key | Allowed range | Notes |
|---|---|---|
| `max_unused_access_keys_days` | `30..180` days | CIS AWS 1.13 recommends 45; NIST IA-5 ≤90 |
| `max_console_access_days` | `30..180` days | CIS AWS 1.12 recommends 45 |
| `max_unused_sagemaker_access_days` | `7..180` days | |
| `max_security_group_rules` | `1..1000` | AWS hard limit is 1000 rules per security group |
| `max_ec2_instance_age_in_days` | `1..1095` days | 3 years |
| `ec2_high_risk_ports` | each port `1..65535` | port 0 is reserved |
| `max_idle_disconnect_timeout_in_seconds` | `60..1800` s | NIST AC-12: cap at 30 min |
| `max_disconnect_timeout_in_seconds` | `60..3600` s | |
| `max_session_duration_seconds` | `600..86400` s | 10 min .. 24 h (AppStream per-session hard limit) |
| `lambda_min_azs` | `1..6` | |
| `recommended_cdk_bootstrap_version` | `1..100` | |
| `log_group_retention_days` | one of `1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653` | only the CloudWatch Logs API-accepted retention values |
| `threat_detection_privilege_escalation_threshold` | `0.0..1.0` | fraction of suspicious actions |
| `threat_detection_privilege_escalation_minutes` | `5..43200` min | under 5 min the signal is mostly false positives |
| `threat_detection_enumeration_threshold` | `0.0..1.0` | |
| `threat_detection_enumeration_minutes` | `5..43200` min | |
| `threat_detection_llm_jacking_threshold` | `0.0..1.0` | |
| `threat_detection_llm_jacking_minutes` | `5..43200` min | |
| `days_to_expire_threshold` (ACM) | `7..365` days | PCI-DSS 4.2.1.1: alert ≥30 days before expiry |
| `elb_min_azs` | `1..6` | |
| `elbv2_min_azs` | `1..6` | |
| `minimum_snapshot_retention_period` | `1..35` days | ElastiCache service hard limit |
| `max_days_secret_unused` | `7..365` days | |
| `max_days_secret_unrotated` | `1..180` days | NIST IA-5: rotate quarterly; CIS ≤90 |
| `min_kinesis_stream_retention_hours` | `24..8760` h | 1 day .. 1 year |
| `detect_secrets_plugins[].limit` | `0.0..10.0` | Shannon entropy threshold |
| `shodan_api_key` | ≤512 chars | |
### Azure
| Key | Allowed range | Notes |
|---|---|---|
| `vm_backup_min_daily_retention_days` | `7..9999` days | Azure Backup hard limit; under 7 days defeats DR/ransomware recovery |
| `apim_threat_detection_llm_jacking_threshold` | `0.0..1.0` | fraction of suspicious actions |
| `apim_threat_detection_llm_jacking_minutes` | `5..43200` min | under 5 min the signal is mostly false positives |
| `shodan_api_key` | ≤512 chars | |
### GCP
| Key | Allowed range | Notes |
|---|---|---|
| `mig_min_zones` | `1..5` | |
| `max_snapshot_age_days` | `1..1095` days | 3 years |
| `max_unused_account_days` | `30..365` days | |
| `storage_min_retention_days` | `1..3650` days | |
| `shodan_api_key` | ≤512 chars | |
### Kubernetes
| Key | Allowed range | Notes |
|---|---|---|
| `audit_log_maxbackup` | `2..1000` | CIS Kubernetes 1.2.18 recommends ≥10 |
| `audit_log_maxsize` | `10..10000` MB | CIS Kubernetes 1.2.19 recommends ≥100 MB |
| `audit_log_maxage` | `7..3650` days | CIS Kubernetes 1.2.17 recommends ≥30 days |
### M365
| Key | Allowed range | Notes |
|---|---|---|
| `sign_in_frequency` | `1..168` h | 1 h .. 7 days; Conditional Access baseline for admins ≤24 h |
| `recommended_mailtips_large_audience_threshold` | `5..10000` | Microsoft default 25 |
| `audit_log_age` | `30..3650` days | M365 E3 default 90 days; SEC/FINRA require ≥7 years |
### GitHub
| Key | Allowed range | Notes |
|---|---|---|
| `inactive_not_archived_days_threshold` | `30..3650` days | CIS GitHub recommends 180 |
### Cloudflare
| Key | Allowed range | Notes |
|---|---|---|
| `max_retries` | `0..10` | 0 disables retries |
### MongoDB Atlas
| Key | Allowed range | Notes |
|---|---|---|
| `max_service_account_secret_validity_hours` | `1..720` h | 1 h .. 30 days |
### Vercel
| Key | Allowed range | Notes |
|---|---|---|
| `days_to_expire_threshold` | `7..365` days | PCI-DSS 4.2.1.1: alert ≥30 days before expiry |
| `stale_token_threshold_days` | `30..3650` days | NIST AC-2(3) typical window 30..90 days |
| `stale_invitation_threshold_days` | `7..365` days | |
| `max_owner_percentage` | `1..50` % | guidance recommends ≤25% |
| `max_owners` | `1..1000` | absolute cap, overrides percentage for large teams |
These bounds live in the provider schemas under `prowler/config/schema/`; each field's `Field(ge=..., le=...)` (or `field_validator`) is the source of truth and the descriptions there carry the full rationale.
This approach ensures that checks are easily configurable, making Prowler highly adaptable to different environments and requirements.
+2 -2
View File
@@ -221,9 +221,9 @@ Before running E2E tests:
```
- **Ensure Prowler API is available**
- By default, Playwright uses `UI_API_BASE_URL=http://localhost:8080/api/v1` (configured in `playwright.config.ts`).
- By default, Playwright uses `NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1` (configured in `playwright.config.ts`).
- Start Prowler API so it is reachable on that URL (for example, via `docker-compose-dev.yml` or the development orchestration used locally).
- If a different API URL is required, set `UI_API_BASE_URL` accordingly before running the tests.
- If a different API URL is required, set `NEXT_PUBLIC_API_BASE_URL` accordingly before running the tests.
- **Ensure Prowler App UI is available**
- Playwright automatically starts the Next.js server through the `webServer` block in `playwright.config.ts` (`pnpm run dev` by default).
@@ -1,53 +0,0 @@
---
title: 'Environment Variable Naming Convention'
---
Prowler is a monorepo composed of several runtime components — Prowler App (the web user interface), Prowler API (the backend), Prowler SDK, and Prowler MCP Server (Model Context Protocol) — that frequently share a single `.env` file. To keep that shared configuration unambiguous, each component namespaces its environment variables with a component-specific prefix.
## Component Prefixes
Each component owns a dedicated prefix for the environment variables it reads:
| Component | Prefix | Status |
|-----------|--------|--------|
| Prowler App (web UI) | `UI_` | Adopted |
| Prowler API (backend) | `API_` | Planned |
| Prowler SDK | `SDK_` | Planned |
| Prowler MCP Server | `MCP_` | Planned |
## Why Component Prefixes Matter
Component prefixes solve three concrete problems in a shared configuration file:
- **Collisions in a shared `.env`:** Several components historically read identically named variables. The API base URL, for example, is consumed by more than one component, so a single unprefixed name is ambiguous. A component prefix removes that ambiguity.
- **Explicit ownership:** A prefix states, at a glance, which component consumes a variable.
- **Reduced accidental exposure:** For Prowler App, scoping browser-facing configuration under one intentional prefix prevents server-only values from leaking into the client bundle.
## Prowler App
Prowler App has adopted the `UI_` prefix. Its public configuration is resolved from the container environment at runtime rather than inlined at build time, so a single pre-built image serves any deployment. For the operational details on changing these values without rebuilding the image, see [Troubleshooting](/troubleshooting).
The former build-time variables map to the new runtime variables as follows:
| Former variable | New variable |
|-----------------|--------------|
| `NEXT_PUBLIC_API_BASE_URL` | `UI_API_BASE_URL` |
| `NEXT_PUBLIC_API_DOCS_URL` | `UI_API_DOCS_URL` |
| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | `UI_GOOGLE_TAG_MANAGER_ID` |
| `NEXT_PUBLIC_SENTRY_DSN`, `SENTRY_DSN` | `UI_SENTRY_DSN` |
| `NEXT_PUBLIC_SENTRY_ENVIRONMENT`, `SENTRY_ENVIRONMENT` | `UI_SENTRY_ENVIRONMENT` |
The build-time-only Sentry variables used for source-map upload — `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`, and `SENTRY_RELEASE` — keep their names, as they are not part of the App's runtime configuration.
## Upcoming Breaking Change
<Warning>
Adopting the `API_`, `SDK_`, and `MCP_` prefixes for Prowler API, Prowler SDK, and Prowler MCP Server is a planned breaking change in a future release. Migrate environment configuration to the new names when upgrading.
</Warning>
Prowler API, Prowler SDK, and Prowler MCP Server have not yet adopted the convention. In a future release, the variables each of these components reads will be namespaced under `API_`, `SDK_`, and `MCP_` respectively. The per-component mapping from current to prefixed names will be documented when each change is released.
## Deprecated Names
- **Prowler App:** The bare server-side `SENTRY_DSN` and `SENTRY_ENVIRONMENT` are no longer read; the server and edge runtimes now read `UI_SENTRY_DSN` and `UI_SENTRY_ENVIRONMENT`. The former `NEXT_PUBLIC_*` build-time variables are deprecated but still read at runtime as a fallback when the matching `UI_*` variable is unset. This fallback will be removed in a future release, so set the `UI_*` runtime variables on the running container.
- **Prowler API, Prowler SDK, and Prowler MCP Server:** The current, unprefixed variable names are deprecated. They continue to work today and will be removed once the prefixed convention is adopted for each component, as described in [Upcoming Breaking Change](#upcoming-breaking-change).
-33
View File
@@ -108,39 +108,6 @@ uv sync
source .venv/bin/activate
```
### Running the Local API Development Stack
For API development, Prowler provides a Makefile-based local stack in addition to the manual and Docker Compose workflows documented in the API README. PostgreSQL, Valkey, and Neo4j run with Docker Compose, while Django and the Celery worker run natively through `uv`.
Before using this method, ensure `docker compose`, `tmux`, and `uv` are installed.
This workflow is designed for macOS and should also work on Linux when Docker, `tmux`, and `uv` are available. Windows requires script changes before it can be supported.
To start the local API stack, run:
```shell
make dev
```
This command starts the required services, creates a `tmux` session with panes for the API, worker, and PostgreSQL logs, waits until the API responds, and prints the API URL and log file paths. The API is available at:
```text
http://localhost:8080/api/v1
```
Use these commands to manage the stack:
```shell
make dev-setup # Bootstrap dependencies, migrations, and fixtures
make dev-attach # Attach to the tmux session
make dev-launch # Start the stack on fixed ports and attach
make dev-stop # Stop the tmux session and containers
make dev-clean # Remove stopped development containers
make dev-wipe # Stop everything and delete local development data
make dev-status # Show development container status
```
The UI is not started by this workflow. Start it separately by following the UI development instructions in the `ui/` directory.
### Pre-Commit Hooks
@@ -38,7 +38,7 @@ Before adding a new framework, complete the following checks:
- **Verify the framework is not already supported.** Inspect `prowler/compliance/` and every `prowler/compliance/<provider>/` for an existing JSON file matching the name and version.
- **Confirm the required checks exist.** Every requirement that can be automated must point to one or more existing Prowler checks. For each missing check, implement it first by following the [Prowler Checks](/developer-guide/checks) guide.
- **Review a reference framework.** Use an existing framework with a similar structure as your template:
- Universal: `prowler/compliance/dora_2022_2554.json`, `prowler/compliance/csa_ccm_4.0.json`.
- Universal: `prowler/compliance/dora.json`, `prowler/compliance/csa_ccm_4.0.json`.
- Legacy: `prowler/compliance/aws/cis_2.0_aws.json` (canonical CIS shape), `prowler/compliance/aws/ccc_aws.json`, `prowler/compliance/aws/ens_rd2022_aws.json`, `prowler/compliance/aws/nist_800_53_revision_5_aws.json`.
## Universal Compliance Framework
@@ -51,9 +51,9 @@ Place the file at the top level of the compliance directory:
prowler/compliance/<framework_name>.json
```
Examples in the repository: `prowler/compliance/csa_ccm_4.0.json`, `prowler/compliance/dora_2022_2554.json`.
Examples in the repository: `prowler/compliance/csa_ccm_4.0.json`, `prowler/compliance/dora.json`.
The file is auto-discovered — there is **no** need to register it in any `__init__.py`, modify `prowler/lib/outputs/`, or update any other Python module. The framework key Prowler CLI accepts via `--compliance` is the basename of the JSON file without `.json` (`dora_2022_2554.json` → `dora_2022_2554`).
The file is auto-discovered — there is **no** need to register it in any `__init__.py`, modify `prowler/lib/outputs/`, or update any other Python module. The framework key Prowler CLI accepts via `--compliance` is the basename of the JSON file without `.json` (`dora.json` → `dora`).
### Top-level structure
@@ -70,7 +70,7 @@ The file is auto-discovered — there is **no** need to register it in any `__in
}
```
A `provider` field at the top level is **optional**. The framework's effective provider list is derived by `ComplianceFramework.get_providers()` (`compliance_models.py:739`) from the union of all keys appearing in `requirement.checks` across all requirements; the explicit `provider` field is used **only as a fallback** when no requirement carries any `checks` key. This is what enables a single file (e.g. `dora_2022_2554.json`) to cover AWS today and add Azure / GCP / etc. tomorrow without restructuring.
A `provider` field at the top level is **optional**. The framework's effective provider list is derived by `ComplianceFramework.get_providers()` (`compliance_models.py:739`) from the union of all keys appearing in `requirement.checks` across all requirements; the explicit `provider` field is used **only as a fallback** when no requirement carries any `checks` key. This is what enables a single file (e.g. `dora.json`) to cover AWS today and add Azure / GCP / etc. tomorrow without restructuring.
Provider keys inside `requirement.checks` must match the directory names under `prowler/providers/`. The valid keys at present are: `aws`, `azure`, `gcp`, `m365`, `kubernetes`, `iac`, `github`, `googleworkspace`, `alibabacloud`, `cloudflare`, `mongodbatlas`, `nhn`, `openstack`, `oraclecloud`, `llm`. Comparison in `supports_provider()` is case-insensitive, but lowercase is the convention used everywhere in the repository.
@@ -493,7 +493,7 @@ Before opening a PR, validate the JSON loads cleanly against the model and that
### 1. Schema validation
For **universal** frameworks, load the file and inspect what was parsed. The framework key inside `bulk` is the **basename of the JSON file** (without `.json`); for `prowler/compliance/dora_2022_2554.json` that key is `dora_2022_2554`, for `prowler/compliance/aws/cis_5.0_aws.json` it is `cis_5.0_aws`.
For **universal** frameworks, load the file and inspect what was parsed. The framework key inside `bulk` is the **basename of the JSON file** (without `.json`); for `prowler/compliance/dora.json` that key is `dora`, for `prowler/compliance/aws/cis_5.0_aws.json` it is `cis_5.0_aws`.
```python
from prowler.lib.check.compliance_models import (
@@ -619,7 +619,7 @@ The following issues are the most common when contributing a compliance framewor
Use the following files as templates when modeling a new contribution.
- `prowler/compliance/dora_2022_2554.json` — universal schema, single-provider populated (AWS), ready to extend with more providers.
- `prowler/compliance/dora.json` — universal schema, single-provider populated (AWS), ready to extend with more providers.
- `prowler/compliance/csa_ccm_4.0.json` — universal schema, multi-provider populated (AWS, Azure, GCP, AlibabaCloud, OracleCloud).
- `prowler/compliance/aws/cis_2.0_aws.json` — legacy CIS attribute shape.
- `prowler/compliance/aws/nist_800_53_revision_5_aws.json` — legacy generic attribute shape.
-241
View File
@@ -1,241 +0,0 @@
---
title: 'Server-Sent Events (SSE)'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="1.32.0" />
This guide explains how to add a **Server-Sent Events (SSE)** endpoint to the Prowler API. SSE lets the backend push a one-way stream of events to a client over a single long-lived HTTP connection — ideal for live progress, token-by-token LLM output, or any "the server has news for you" use case where the client should not poll.
<Info>
The platform ships the SSE **infrastructure** (`api.sse`) and wiring. No feature endpoint streams over SSE out of the box — this guide shows how to build one on top of the shared base.
</Info>
## When to use SSE
| Need | Use |
|------|-----|
| Server pushes incremental updates, client only reads | **SSE** |
| Bidirectional, low-latency messaging (chat both ways, games) | WebSocket |
| Client asks, server answers once | Plain REST |
SSE is the right tool when the **client only consumes**: scan progress, long-running job checkpoints, streamed LLM tokens, cross-client resource-sync notifications. It rides on plain HTTP, reconnects automatically in the browser via the native [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API, and needs no extra protocol.
## How it works
SSE is wired through [`django-eventstream`](https://github.com/fanout/django_eventstream) and a small platform layer in `api/src/backend/api/sse/`:
| Piece | File | Responsibility |
|-------|------|----------------|
| `BaseSSEViewSet` | `api/sse/base_views.py` | Base DRF viewset a feature subclasses. The feature implements `get_channels`; the base handles auth, the tenant transaction, and delegates streaming to `django-eventstream`. |
| `SSEChannelManager` | `api/sse/channelmanager.py` | Registered in `settings.EVENTSTREAM_CHANNELMANAGER_CLASS`. Reads the channel set off the request and enforces the platform-wide tenant gate. |
| `SSEAuthentication` | `api/authentication.py` | Same JWT/API-key stack as the rest of the API, plus an `?access_token=<jwt>` fallback for browser `EventSource` clients. Lives with the other authentication classes, not in the `sse` package. |
| `make_channel_name` / `tenant_id_from_channel` | `api/sse/utils.py` | Single source of truth for the channel-name format, so publishers and the channel manager agree byte-for-byte. |
| Settings | `config/settings/eventstream.py` | Valkey Pub/Sub backend (dedicated DB), channel manager, allowed headers. |
### Transport: the server runs on ASGI
SSE connections are long-lived. Holding one open per synchronous worker would exhaust the worker pool, so the API runs under Gunicorn's native **`asgi` worker** (`config.asgi:application`). Streams are parked on the event loop while ordinary CRUD endpoints keep their synchronous execution (Django runs sync views in a thread-sensitive executor under ASGI). This is configured in `config/guniconf.py` and used by both the dev and production entrypoints — no separate server process is needed.
### The data flow
```
publisher (Celery task / view) subscriber (browser, CLI)
│ │
│ send_event(channel, "scan.progress", …) │ GET …/event-stream
▼ ▼
Valkey Pub/Sub ◄────────────────────► BaseSSEViewSet.list
(EVENTSTREAM_VALKEY_DB) → get_channels() (RLS-scoped)
→ SSEChannelManager (tenant gate)
→ StreamingHttpResponse (text/event-stream)
```
A publisher anywhere in the system (most often a Celery task) calls `send_event(channel, event_type, payload)`. `django-eventstream` fans it out over Valkey Pub/Sub to every connection subscribed to that channel.
## Adding an SSE endpoint to your feature
The example below streams progress for a long-running **scan**. Adapt the resource, prefix, and event names to your feature.
<Steps>
<Step title="Pick a channel prefix">
Channels follow the format `<prefix>:<tenant_id>:<resource_id>`, built only through `make_channel_name`. The prefix is owned by your feature and may contain hyphens but **never colons** (the parser splits on `:`).
```python
CHANNEL_PREFIX = "scan-progress"
```
The tenant id is baked into every channel name. That is what lets the platform enforce cross-tenant isolation without knowing anything about your feature.
</Step>
<Step title="Subclass BaseSSEViewSet">
Create the viewset for the SSE sub-resource. The only required method is `get_channels`; it runs inside the tenant transaction set up by the base class, so any database lookup inside it is automatically RLS-scoped.
```python
# scans/event_streams.py
from api.sse import BaseSSEViewSet, make_channel_name
from django.shortcuts import get_object_or_404
from scans.models import Scan
CHANNEL_PREFIX = "scan-progress"
class ScanEventStreamViewSet(BaseSSEViewSet):
def get_queryset(self):
# RLS already scopes to the tenant; narrow further as needed
# (e.g. only scans the requesting user may see).
return Scan.objects.filter(tenant_id=self.request.tenant_id)
def get_channels(self) -> set[str]:
scan = get_object_or_404(self.get_queryset(), pk=self.kwargs["scan_pk"])
return {make_channel_name(CHANNEL_PREFIX, scan.tenant_id, scan.id)}
```
<Warning>
`get_channels` **must raise** the relevant DRF exception (`NotFound`, `PermissionDenied`, `NotAuthenticated`) when authorization fails — `get_object_or_404` does this for you. Returning an empty set surfaces as django-eventstream's confusing "No channels specified" error instead of the real cause.
</Warning>
</Step>
<Step title="Wire the URL as a sub-resource">
Mount the endpoint as an `event-stream` sub-resource. Keep it **outside the DRF router**, which would force the URL into a list/detail convention. Route the `get` method to the viewset's `list` action.
```python
# scans/urls.py
path(
"scans/<uuid:scan_pk>/event-stream",
ScanEventStreamViewSet.as_view({"get": "list"}),
name="scan-event-stream",
),
```
</Step>
<Step title="Define your event vocabulary">
A feature owns its event types in `<app>/<domain>/events.py`: one `publish_<event>` function per event type, each body a **single** `send_event` call so the wire-level string lives in exactly one place.
```python
# scans/events.py
from django_eventstream import send_event
def publish_progress(channel: str, checked: int, total: int) -> None:
send_event(channel, "scan.progress", {"checked": checked, "total": total})
def publish_end(channel: str, scan_id: str) -> None:
# Terminal event carries the canonical id so reconnecting clients
# can refetch the persisted resource over REST.
send_event(channel, "scan.end", {"scan_id": scan_id})
def publish_error(channel: str, code: str, detail: str) -> None:
send_event(channel, "scan.error", {"code": code, "detail": detail})
```
There is no platform-side enum, registry, or dispatch table — **the naming convention is the contract** (see below).
</Step>
<Step title="Publish from the producer">
Wherever the work happens — usually a Celery task — build the channel the same way and publish:
```python
from api.sse import make_channel_name
from scans.events import publish_progress, publish_end
channel = make_channel_name("scan-progress", scan.tenant_id, scan.id)
publish_progress(channel, checked=42, total=100)
...
publish_end(channel, scan_id=str(scan.id))
```
</Step>
</Steps>
## Event naming convention
Every event uses an event type of the form **`<resource>.<verb>`** (lowercased, dot-separated). The verb comes from this platform-wide vocabulary — if you need a verb that is not listed, document the addition in this guide so the catalog stays discoverable.
| Verb | When to use |
|------|-------------|
| `delta` | An incremental piece of a stream the client concatenates (LLM text tokens, audio chunks). Standard term across OpenAI / Anthropic / LiteLLM / Vercel AI SDK. |
| `start` | Begin marker for a compound operation (e.g. a tool call whose execution will be reported by a matching `end`). |
| `end` | Terminal marker. Carries the canonical resource id so reconnecting clients can refetch persisted state via REST. |
| `progress` | Periodic checkpoint with quantifiable completion, e.g. `{"checked": 42, "total": 100}`. |
| `created` / `updated` / `deleted` | Resource-lifecycle events for cross-client sync streams. |
| `error` | Terminal failure. Carries a stable `code` for client switching and a human-readable `detail`. |
<Note>
Payloads are **flat JSON**. The wire-level `event:` field already names the event type, so do **not** wrap the payload in `{"type": ..., "data": ...}`. Include the canonical resource UUID on terminal events so reconnecting clients can reconcile via REST.
</Note>
## Authentication
SSE endpoints use the same authentication stack as the rest of the API. Non-browser clients (CLI, programmatic) send the standard `Authorization` header — JWT or API key.
Browser `EventSource` is the only widely available SSE client API and it **cannot set custom headers**. For that case only, the endpoint accepts a JWT via the `?access_token=<jwt>` query parameter. The header always wins when present — a header is intentional, while a query parameter can leak into referers and logs, so it is consulted only as a fallback.
```javascript
// Browser
const es = new EventSource(
`/api/v1/scans/${scanId}/event-stream?access_token=${jwt}`
);
```
```bash
# CLI / programmatic — header, exactly like every other endpoint
curl -N -H "Authorization: Bearer $JWT" \
https://<host>/api/v1/scans/$SCAN_ID/event-stream
```
## Tenant isolation & security model
Authorization is enforced at two layers:
1. **At connect**, `get_channels` runs under the regular DRF stack inside the tenant transaction (`rls_transaction`). Resource lookups are RLS-scoped, so a user cannot even resolve a channel for a resource they cannot see. Narrow the queryset further (e.g. `created_by=request.user`) when a resource is per-user within a tenant.
2. **After connect**, `SSEChannelManager.can_read_channel` re-verifies tenant membership by parsing the tenant id embedded in the channel name. Cross-tenant subscription is rejected even if a URL-level check ever has a bug. A malformed channel name is treated as "not authorized".
Because the tenant id lives inside the channel name, this gate works for any feature without the platform knowing anything about it.
## Reconnect & state recovery
The platform deliberately ships **without server-side replay** (`is_channel_reliable` returns `False`). When a client reconnects, it does **not** receive missed events. Instead:
- Terminal events (`*.end`) carry the canonical resource **UUID**.
- On reconnect, the client refetches the authoritative state from the normal REST endpoint using that id.
Design your event payloads accordingly: deltas are ephemeral and concatenated in-flight; the durable truth always lives behind a REST resource.
## Local development
- The dev and production entrypoints both launch Gunicorn with the `asgi` worker (`config.asgi:application`). In dev, `DJANGO_DEBUG=True` enables hot reload; `preload_app` is automatically disabled under debug so edited code is picked up.
- SSE uses a **dedicated Valkey database** (`EVENTSTREAM_VALKEY_DB`, default `2`) kept separate from the Celery broker so a noisy broker cannot crowd out streaming traffic. It reuses the same `VALKEY_*` connection settings as the rest of the platform.
| Env var | Default | Purpose |
|---------|---------|---------|
| `EVENTSTREAM_VALKEY_DB` | `2` | Valkey DB index for the SSE Pub/Sub bus |
| `DJANGO_WORKER_CLASS` | `asgi` | Gunicorn worker class |
Test the stream end to end with `curl -N` (disable buffering) and an auth header:
```bash
curl -N -H "Authorization: Bearer $JWT" \
http://localhost:8080/api/v1/scans/$SCAN_ID/event-stream
```
## Testing
The platform basis is covered by `api/tests/test_sse.py` (channel parsing, the tenant gate, and auth precedence). For a feature endpoint, test:
- `get_channels` returns the expected channel for an authorized resource and raises `NotFound`/`PermissionDenied` otherwise.
- Each `publish_<event>` helper emits the correct event type and flat payload (mock `send_event`).
- The producer builds the channel with `make_channel_name` using the resource's own `tenant_id`.
+1 -10
View File
@@ -359,13 +359,6 @@
"user-guide/providers/okta/getting-started-okta",
"user-guide/providers/okta/authentication"
]
},
{
"group": "Linode",
"pages": [
"user-guide/providers/linode/getting-started-linode",
"user-guide/providers/linode/authentication"
]
}
]
},
@@ -402,8 +395,7 @@
"developer-guide/lighthouse-architecture",
"developer-guide/mcp-server",
"developer-guide/ai-skills",
"developer-guide/prowler-studio",
"developer-guide/server-sent-events"
"developer-guide/prowler-studio"
]
},
{
@@ -424,7 +416,6 @@
"group": "Miscellaneous",
"pages": [
"developer-guide/documentation",
"developer-guide/environment-variables",
{
"group": "Testing",
"pages": [
@@ -128,8 +128,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.30.0"
PROWLER_API_VERSION="5.30.0"
PROWLER_UI_VERSION="5.29.0"
PROWLER_API_VERSION="5.29.0"
```
<Note>
@@ -4,7 +4,7 @@ title: 'Installation'
## Installation
To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/):
To install Prowler as a Python package, use `Python >= 3.10, <= 3.12`. Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/):
<Tabs>
<Tab title="pipx">
@@ -12,7 +12,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
_Requirements_:
* `Python >= 3.10, <= 3.13`
* `Python >= 3.10, <= 3.12`
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
* AWS, GCP, Azure and/or Kubernetes credentials
@@ -30,7 +30,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
_Requirements_:
* `Python >= 3.10, <= 3.13`
* `Python >= 3.10, <= 3.12`
* `Python pip >= 21.0.0`
* AWS, GCP, Azure, M365 and/or Kubernetes credentials
@@ -81,7 +81,7 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
<Tab title="Amazon Linux 2">
_Requirements_:
* `Python >= 3.10, <= 3.13`
* `Python >= 3.10, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
@@ -96,8 +96,8 @@ To install Prowler as a Python package, use `Python >= 3.10, <= 3.13`. Prowler i
<Tab title="Ubuntu">
_Requirements_:
* `Ubuntu 23.04` or above. For older Ubuntu versions, check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure `Python >= 3.10, <= 3.13` is installed.
* `Python >= 3.10, <= 3.13`
* `Ubuntu 23.04` or above. For older Ubuntu versions, check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure `Python >= 3.10, <= 3.12` is installed.
* `Python >= 3.10, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
+20 -49
View File
@@ -2,8 +2,6 @@
title: 'Troubleshooting'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
## Running `prowler` I get `[File: utils.py:15] [Module: utils] CRITICAL: path/redacted: OSError[13]`
That is an error related to file descriptors or opened files allowed by your operating system.
@@ -83,39 +81,6 @@ docker compose down
docker compose up -d
```
### Worker Uses Too Much Memory on Hosts with Many CPUs
<VersionBadge version="5.31.0" />
When Prowler App runs self-hosted on a machine or Kubernetes node with many CPUs,
the Celery worker may create one prefork process per detected CPU if concurrency
is not configured explicitly. Each process loads the SDK runtime and cloud
provider clients, so idle memory can be high and worker containers can be
terminated by their memory limit.
Set `DJANGO_CELERY_WORKER_CONCURRENCY` in the worker runtime environment to cap
the number of prefork processes:
```yaml
services:
worker:
environment:
DJANGO_CELERY_WORKER_CONCURRENCY: "4"
```
For Kubernetes deployments, set the same variable on the worker Deployment:
```yaml
env:
- name: DJANGO_CELERY_WORKER_CONCURRENCY
value: "4"
```
Lower values reduce idle memory and the number of tasks a worker can run in
parallel. Increase the value only when the worker has enough memory for the
expected scan workload. Leaving the variable unset preserves Celery's default
CPU-based concurrency.
### API Container Fails to Start with JWT Key Permission Error
See [GitHub Issue #8897](https://github.com/prowler-cloud/prowler/issues/8897) for more details.
@@ -236,29 +201,35 @@ When running Prowler behind a reverse proxy (nginx, Traefik, etc.) or load balan
**Root Cause:**
The API base and docs URLs are resolved from the container environment **at runtime**. A single pre-built Docker image (`prowlercloud/prowler-ui:stable`) therefore serves any environment: point the URLs at your external domain and restart the container — no rebuild is required.
Next.js environment variables prefixed with `NEXT_PUBLIC_` are **bundled at build time**, not runtime. The pre-built Docker images from Docker Hub (`prowlercloud/prowler-ui:stable`) are built with default internal URLs. Simply setting `NEXT_PUBLIC_API_BASE_URL` in your `.env` file or environment variables and restarting the container will **NOT** work because these values are already compiled into the JavaScript bundle.
**Solution:**
Set the runtime environment variables to your external URL and restart the UI container:
You must **rebuild** the UI Docker image with your external URL:
```bash
# Clone the repository (if you haven't already)
git clone https://github.com/prowler-cloud/prowler.git
cd prowler/ui
# Build with your external URL as a build argument
docker build \
--build-arg NEXT_PUBLIC_API_BASE_URL=https://prowler.example.com/api/v1 \
--build-arg NEXT_PUBLIC_API_DOCS_URL=https://prowler.example.com/api/v1/docs \
-t prowler-ui-custom:latest \
--target prod \
.
```
Then update your `docker-compose.yml` to use your custom image instead of the pre-built one:
```yaml
services:
ui:
image: prowlercloud/prowler-ui:stable
environment:
UI_API_BASE_URL: https://prowler.example.com/api/v1
UI_API_DOCS_URL: https://prowler.example.com/api/v1/docs
image: prowler-ui-custom:latest # Use your custom-built image
# ... rest of configuration
```
The same values can be supplied through your `.env` file:
```bash
UI_API_BASE_URL=https://prowler.example.com/api/v1
UI_API_DOCS_URL=https://prowler.example.com/api/v1/docs
```
<Note>
Earlier releases inlined these values into the JavaScript bundle at build time (via the `NEXT_PUBLIC_` prefix) and required a rebuild with `--build-arg`. That is no longer necessary: `UI_API_BASE_URL` and `UI_API_DOCS_URL` are read at container start, so updating them and restarting is sufficient.
The `NEXT_PUBLIC_` prefix is a Next.js convention that exposes environment variables to the browser. Since the browser bundle is compiled during `docker build`, these variables must be provided as build arguments, not runtime environment variables.
</Note>
@@ -10,20 +10,14 @@ prowler/config/config.yaml
Additionally, you can input a custom configuration file using the `--config-file` argument.
<Note>
Numeric thresholds enforce hard limits. A value outside the accepted range is dropped with a warning and the check falls back to its built-in default. See [Configuration Value Limits](/developer-guide/configurable-checks#configuration-value-limits) for the exact range of every bounded option (max-days caps, percentages, counts, etc.).
</Note>
## AWS
### Configurable Checks
The following list includes all the AWS checks with configurable variables that can be changed in the configuration yaml file:
| Check Name | Value | Type |
|---------------------------------------------------------------|--------------------------------------------------|-----------------|
| `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer |
| `acmpca_certificate_authority_pqc_key_algorithm` | `acmpca_pqc_key_algorithms` | List of Strings |
| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer |
| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer |
| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer |
@@ -61,9 +55,6 @@ The following list includes all the AWS checks with configurable variables that
| `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer |
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer |
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer |
| `rolesanywhere_trust_anchor_pqc_pki` | `rolesanywhere_pqc_pca_key_algorithms` | List of Strings |
| `cloudfront_distributions_pqc_tls_enabled` | `cloudfront_pqc_min_protocol_versions` | List of Strings |
| `apigateway_domain_name_pqc_tls_enabled` | `apigateway_pqc_tls_allowed_policies` | List of Strings |
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
| `iam_user_access_not_stale_to_sagemaker` | `max_unused_sagemaker_access_days` | Integer |
| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer |
@@ -76,7 +67,6 @@ The following list includes all the AWS checks with configurable variables that
| `secretsmanager_secret_rotated_periodically` | `max_days_secret_unrotated` | Integer |
| `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings |
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
| `transfer_server_pqc_ssh_kex_enabled` | `transfer_pqc_ssh_allowed_policies` | List of Strings |
| `dynamodb_table_cross_account_access` | `trusted_account_ids` | List of Strings |
| `eventbridge_bus_cross_account_access` | `trusted_account_ids` | List of Strings |
| `eventbridge_schema_registry_cross_account_access` | `trusted_account_ids` | List of Strings |
+2 -2
View File
@@ -127,7 +127,7 @@ Add the following to `.gitlab-ci.yml`:
```yaml
prowler-scan:
image: python:3.13-slim
image: python:3.12-slim
stage: test
script:
- pip install prowler
@@ -154,7 +154,7 @@ stages:
- security
.prowler-base:
image: python:3.13-slim
image: python:3.12-slim
stage: security
before_script:
- pip install prowler
@@ -1,97 +0,0 @@
---
title: "Linode Authentication in Prowler"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.31.0" />
Prowler for Linode uses a **Personal Access Token** (PAT) for authentication. Prowler reads the token **exclusively** from the `LINODE_TOKEN` environment variable, so the secret is never exposed in shell history or process listings. There are no credential CLI flags.
## Required Permissions
Prowler requires read-only access to your Linode account. The following OAuth scopes are needed on the Personal Access Token:
| Scope | Access | Description |
|-------|--------|-------------|
| `account` | `Read Only` | Required to list users and verify account identity |
| `linodes` | `Read Only` | Required to list instances and their configurations |
| `firewall` | `Read Only` | Required to list firewalls and their rules |
<Warning>
Ensure the token has all required scopes. Missing permissions will cause some checks to fail or return incomplete results.
</Warning>
---
## Personal Access Token
### Step 1: Create a Personal Access Token
1. Log into the [Linode Cloud Manager](https://cloud.linode.com).
2. Click on your username in the top-right corner, then select **API Tokens** under the "My Profile" section.
3. Click **Create a Personal Access Token**.
4. Configure the token:
- **Label:** A descriptive name (e.g., "Prowler Security Scanner")
- **Expiry:** Set an appropriate expiration (e.g., 6 months)
- **Permissions:** Set the following scopes to **Read Only**:
- Account
- Linodes
- Firewall
- All other scopes can be set to **No Access**
5. Click **Create Token**.
6. Copy the token immediately — it will not be shown again.
### Step 2: Configure Authentication
Set the `LINODE_TOKEN` environment variable:
```bash
export LINODE_TOKEN="your-personal-access-token"
```
Then run Prowler:
```bash
prowler linode
```
---
## Verifying Authentication
To verify that Prowler can connect to your Linode account, run:
```bash
prowler linode --list-checks
```
If authentication succeeds, you will see a list of available checks. If it fails, Prowler will display an error message indicating the credentials issue.
---
## CI/CD Integration
For automated pipelines, set the token as a secret environment variable:
**GitHub Actions:**
```yaml
env:
LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
steps:
- name: Run Prowler
run: prowler linode
```
**GitLab CI:**
```yaml
variables:
LINODE_TOKEN: $LINODE_TOKEN
prowler_scan:
script:
- prowler linode
```
@@ -1,61 +0,0 @@
---
title: 'Getting Started With Linode on Prowler'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.31.0" />
Prowler for Linode scans your Linode infrastructure for security misconfigurations, including compute settings, networking rules, user account security, and more.
<Note>
Linode support in Prowler is community-maintained. For commercial support or to request additional service coverage, [contact us](https://prowler.com/contact).
</Note>
## Prerequisites
Set up authentication for Linode with the [Linode Authentication](/user-guide/providers/linode/authentication) guide before starting:
- Create a Linode Personal Access Token with read-only permissions
- The token requires at minimum: `account:read_only`, `linodes:read_only`, and `firewall:read_only` scopes
## Prowler CLI
### Run Prowler for Linode
Once authenticated with a Personal Access Token, set the `LINODE_TOKEN` environment variable and run Prowler for Linode. Prowler reads the token exclusively from the environment variable, so the secret is never exposed in shell history or process listings:
```bash
export LINODE_TOKEN="your-personal-access-token"
prowler linode
```
### Run Specific Checks
```bash
prowler linode --checks compute_instance_backups_enabled compute_instance_watchdog_enabled
```
### Run a Specific Service
```bash
prowler linode --services networking
```
### Scan Specific Regions
Use `--region` (alias `--filter-region` / `-f`) to limit the scan to one or more Linode regions. Region-less resources (account administration and Cloud Firewalls) are always scanned; only regional resources such as instances are filtered. When the flag is omitted, all regions are scanned.
```bash
prowler linode --region eu-central us-east
```
## Available Services
Prowler for Linode currently supports the following services:
| Service | Description |
|---------|-------------|
| `administration` | Account administration includes users and access controls such as two-factor authentication |
| `compute` | Compute includes Linode instances and their workload configuration |
| `networking` | Networking includes Cloud Firewalls and their stateful network rules |
@@ -108,10 +108,10 @@ Prowler App updates user attributes each time a user logs in. Any changes made i
The `userType` attribute controls which Prowler role is assigned to the user:
- If `userType` matches an existing Prowler role name, the user receives that role automatically.
- If `userType` does not match any existing role, Prowler App creates a new role with that name **with read-only access** (visibility over all providers, no management permissions). A Prowler administrator can adjust its permissions afterward through the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
- If `userType` is not set, the user's existing roles are left unchanged.
- If `userType` does not match any existing role, Prowler App creates a new role with that name **without permissions**.
- If `userType` is not set, the user receives the `no_permissions` role.
The `userType` value is **case-sensitive** - for example, `Backend` and `backend` are treated as different roles.
In all cases where the resulting role has no permissions, a Prowler administrator must configure the appropriate permissions through the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab. The `userType` value is **case-sensitive** - for example, `Backend` and `backend` are treated as different roles.
</Warning>
@@ -223,9 +223,9 @@ To test the `userType` → role mapping, set the **Department** attribute in the
After a successful SSO login, the user profile in Prowler App reflects the attributes sent by Google Workspace:
- **Name**: Populated from the `firstName` and `lastName` attributes.
- **Role**: Created automatically from the `userType` attribute (e.g., `Backend`). If the role did not exist previously, it is created with read-only access by default.
- **Permissions**: If the assigned permissions need to be adjusted, a Prowler administrator can either:
- Edit the permissions of the new role via the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
- **Role**: Created automatically from the `userType` attribute (e.g., `Backend`). If the role did not exist previously, it is created with no permissions by default.
- **Permissions**: In the screenshot below, the user has no permissions because the `Backend` role did not exist prior to login and was created automatically without any permissions. To resolve this, a Prowler administrator can either:
- Assign the appropriate permissions to the new role via the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
- Set the `userType` attribute in the IdP to match an existing Prowler role that already has the desired permissions. The updated role is applied on the next SAML login.
For more details on role assignment behavior and attribute mapping, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso#configure-attribute-mapping-in-the-idp) page.
@@ -87,7 +87,7 @@ Choose a Method:
|----------------|---------------------------------------------------------------------------------------------------------|----------|
| `firstName` | The user's first name. | Yes |
| `lastName` | The user's last name. | Yes |
| `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler App creates a new role with that name with read-only access (visibility over all providers, no management permissions). If `userType` is not defined, the user's existing roles are left unchanged. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No |
| `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler App creates a new role with that name without permissions. If `userType` is not defined, the user is assigned the `no_permissions` role. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No |
| `organization` | The user's company name. | No |
<Info>
@@ -140,7 +140,7 @@ Choose a Method:
![Okta User Profile — First Name and Last Name](/images/prowler-app/saml/okta-user-profile-name.png)
* **Organization** (`organization`): Maps to the company name displayed in Prowler App. This attribute is optional.
* **User type** (`userType`): Determines the Prowler role assigned to the user. This attribute is **case-sensitive**: if it matches the exact name of an existing role in Prowler App the user receives that role; if no role with that name exists, a new one is created with read-only access.
* **User type** (`userType`): Determines the Prowler role assigned to the user. This attribute is **case-sensitive** and must match the exact name of an existing role in Prowler App.
![Okta User Profile — User Type and Organization](/images/prowler-app/saml/okta-user-profile-attributes.png)
@@ -152,10 +152,14 @@ Choose a Method:
The `userType` attribute controls which Prowler role is assigned to the user:
* If a role with the specified name already exists in Prowler App, the user automatically receives that role.
* If the role does not exist, Prowler App creates a new role with that exact name with read-only access: the user can see all providers and their findings but cannot manage anything. A Prowler administrator (a user whose role includes the "Manage Account" permission) can adjust its permissions afterward through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac).
* If `userType` is not defined in the user's Okta profile, the user's existing roles in Prowler App are left unchanged.
* If the role does not exist, Prowler App creates a new role with that exact name but without any permissions, preventing the user from performing any actions.
* If `userType` is not defined in the user's Okta profile, the user is assigned the `no_permissions` role.
**Example:** To assign the `IT` role to a user, set the `userType` value to `IT` in Okta. If a role named `IT` already exists in Prowler App, the user receives it automatically upon login. If it does not exist, Prowler App creates a new role called `IT` with read-only access, and a Prowler administrator can adjust its permissions as needed.
In all cases where the resulting role has no permissions, a Prowler administrator (a user whose role includes the "Manage Account" permission) must configure the appropriate permissions through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac).
This behavior is intentional: by defaulting to no permissions, Prowler App ensures that a misconfiguration in Okta cannot inadvertently grant elevated access.
**Example:** To assign the `IT` role to a user, set the `userType` value to `IT` in Okta. If a role named `IT` already exists in Prowler App, the user receives it automatically upon login. If it does not exist, Prowler App creates a new role called `IT` without permissions, and a Prowler administrator must configure the desired permissions for it.
</Warning>
+2 -2
View File
@@ -1,7 +1,7 @@
# =============================================================================
# Build stage - Install dependencies and build the application
# =============================================================================
FROM ghcr.io/astral-sh/uv:0.11.21-python3.13-alpine3.23@sha256:f09cc61ffc001f202701fdeae14dbdd50f6ca4cfcf248f41fd3234a302c8534f AS builder
FROM ghcr.io/astral-sh/uv:python3.13-alpine@sha256:8f53782bb232ab0b5558f3071e86e2bbfde884e18815f2b19cc57f2d336e9ee2 AS builder
WORKDIR /app
@@ -25,7 +25,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# =============================================================================
# Final stage - Minimal runtime environment
# =============================================================================
FROM python:3.13.14-alpine3.23@sha256:b0513989fa9be54569cac73f48a60320b74bb0f9ffa886568eea7e48a2432c04
FROM python:3.13-alpine@sha256:bb1f2fdb1065c85468775c9d680dcd344f6442a2d1181ef7916b60a623f11d40
LABEL maintainer="https://github.com/prowler-cloud"
+6 -6
View File
@@ -857,11 +857,11 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.13.0"
version = "2.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
]
[package.optional-dependencies]
@@ -1132,15 +1132,15 @@ wheels = [
[[package]]
name = "starlette"
version = "1.3.1"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
]
[[package]]
+1 -5
View File
@@ -36,8 +36,6 @@
"lightsail:GetRelationalDatabases",
"macie2:GetMacieSession",
"macie2:GetAutomatedDiscoveryConfiguration",
"rolesanywhere:ListTagsForResource",
"rolesanywhere:ListTrustAnchors",
"s3:GetAccountPublicAccessBlock",
"shield:DescribeProtection",
"shield:GetSubscriptionState",
@@ -63,9 +61,7 @@
],
"Resource": [
"arn:*:apigateway:*::/restapis/*",
"arn:*:apigateway:*::/apis/*",
"arn:*:apigateway:*::/domainnames",
"arn:*:apigateway:*::/domainnames/*"
"arn:*:apigateway:*::/apis/*"
],
"Sid": "AllowAPIGatewayReadOnly"
}
@@ -129,8 +129,6 @@ Resources:
- "lightsail:GetRelationalDatabases"
- "macie2:GetMacieSession"
- "macie2:GetAutomatedDiscoveryConfiguration"
- "rolesanywhere:ListTagsForResource"
- "rolesanywhere:ListTrustAnchors"
- "s3:GetAccountPublicAccessBlock"
- "shield:DescribeProtection"
- "shield:GetSubscriptionState"
@@ -152,8 +150,6 @@ Resources:
Resource:
- "arn:*:apigateway:*::/restapis/*"
- "arn:*:apigateway:*::/apis/*"
- "arn:*:apigateway:*::/domainnames"
- "arn:*:apigateway:*::/domainnames/*"
- !If
- OrganizationsEnabled
- PolicyName: ProwlerOrganizations
-81
View File
@@ -2,86 +2,6 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.31.1] (Prowler v5.31.1)
### 🐞 Fixed
- Alibaba Cloud `ram_password_policy_number` and `cs_kubernetes_cluster_check_weekly` checks not being loaded due to missing implementation and package files [(#11683)](https://github.com/prowler-cloud/prowler/pull/11683)
---
## [5.31.0] (Prowler v5.31.0)
### 🚀 Added
- Support for Python 3.13 [(#9293)](https://github.com/prowler-cloud/prowler/pull/9293)
- `securityhub_delegated_admin_enabled_all_regions` check for AWS provider, verifying that Security Hub has a delegated administrator, is active in all opted-in regions, and has organization auto-enable on [(#11259)](https://github.com/prowler-cloud/prowler/pull/11259)
- `config_delegated_admin_and_org_aggregator_all_regions` check for AWS provider, verifying that AWS Config has a delegated administrator and an organization aggregator covering all AWS regions [(#11259)](https://github.com/prowler-cloud/prowler/pull/11259)
- `sagemaker_clarify_exists` check for AWS provider [(#11211)](https://github.com/prowler-cloud/prowler/pull/11211)
- `cloudsql_instance_high_availability_enabled` check for GCP provider, verifying Cloud SQL primary instances use `REGIONAL` availability for automatic zone failover [(#11024)](https://github.com/prowler-cloud/prowler/pull/11024)
- `cloudfunction_function_inside_vpc` check for GCP provider, verifying Cloud Functions have a Serverless VPC Access connector for private egress [(#11021)](https://github.com/prowler-cloud/prowler/pull/11021)
- `cloudfunction_function_not_publicly_accessible` check for GCP provider, detecting Cloud Functions with `allUsers` or `allAuthenticatedUsers` IAM invocation bindings [(#11022)](https://github.com/prowler-cloud/prowler/pull/11022)
- `secretmanager_secret_not_publicly_accessible` check for GCP provider, detecting Secret Manager secrets with public IAM bindings [(#11025)](https://github.com/prowler-cloud/prowler/pull/11025)
- `secretmanager_secret_rotation_enabled` check for GCP provider, verifying Secret Manager secrets have automatic rotation configured within 90 days [(#11026)](https://github.com/prowler-cloud/prowler/pull/11026)
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)
- `cosmosdb_account_minimum_tls_version` check for Azure provider, verifying Cosmos DB accounts enforce TLS 1.2 or higher for client connections [(#11033)](https://github.com/prowler-cloud/prowler/pull/11033)
- `cosmosdb_account_public_network_access_disabled` check for Azure provider, verifying Cosmos DB accounts have public network access disabled so connectivity is restricted to private endpoints or VNet service endpoints [(#11034)](https://github.com/prowler-cloud/prowler/pull/11034)
- `databricks_workspace_public_network_access_disabled` check for Azure provider, verifying Databricks workspaces have public network access disabled so connectivity is restricted to Azure Private Link private endpoints [(#11035)](https://github.com/prowler-cloud/prowler/pull/11035)
- `databricks_workspace_no_public_ip_enabled` check for Azure provider, verifying Databricks workspaces use secure cluster connectivity (no public IP) so compute nodes are not assigned public IP addresses [(#11036)](https://github.com/prowler-cloud/prowler/pull/11036)
- `defender_ensure_defender_cspm_is_on` check for Azure provider, verifying Microsoft Defender Cloud Security Posture Management (CSPM) is enabled on the Standard tier [(#11037)](https://github.com/prowler-cloud/prowler/pull/11037)
- `mysql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying MySQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11041)](https://github.com/prowler-cloud/prowler/pull/11041)
- `mysql_flexible_server_high_availability_enabled` check for Azure provider, verifying MySQL Flexible Servers have high availability enabled for automatic failover to a standby replica [(#11042)](https://github.com/prowler-cloud/prowler/pull/11042)
- `postgresql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying PostgreSQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11045)](https://github.com/prowler-cloud/prowler/pull/11045)
- `postgresql_flexible_server_high_availability_enabled` check for Azure provider, verifying PostgreSQL Flexible Servers have high availability enabled for automatic failover to a standby replica [(#11046)](https://github.com/prowler-cloud/prowler/pull/11046)
- `aks_cluster_azure_monitor_enabled` check for Azure provider, verifying AKS clusters have Azure Monitor (Container Insights) enabled for metrics, logs, and alerting [(#11029)](https://github.com/prowler-cloud/prowler/pull/11029)
- `aks_cluster_local_accounts_disabled` check for Azure provider, verifying AKS clusters have local accounts disabled so authentication is forced through Microsoft Entra ID [(#11030)](https://github.com/prowler-cloud/prowler/pull/11030)
- `network_subnet_nsg_associated` check for Azure provider, verifying virtual network subnets have a network security group associated to enforce traffic filtering [(#11043)](https://github.com/prowler-cloud/prowler/pull/11043)
- `network_vnet_ddos_protection_enabled` check for Azure provider, verifying virtual networks have Azure DDoS Network Protection enabled [(#11044)](https://github.com/prowler-cloud/prowler/pull/11044)
- `entra_app_registration_credential_not_expired` check for Azure provider, verifying Entra ID app registration secrets and certificates are not expired, expiring within 30 days, or without an expiration date [(#11038)](https://github.com/prowler-cloud/prowler/pull/11038)
- `entra_authentication_methods_policy_strong_auth_enforced` check for Azure provider, verifying the Entra ID authentication methods policy enforces MFA registration and enables at least one strong method (Microsoft Authenticator, FIDO2, or X.509 certificate) [(#11039)](https://github.com/prowler-cloud/prowler/pull/11039)
- `entra_user_with_recent_sign_in` check for Azure provider, detecting stale enabled accounts that have not signed in within the last 90 days (requires Entra ID P1/P2 licensing for sign-in activity) [(#11040)](https://github.com/prowler-cloud/prowler/pull/11040)
- `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027)
- Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398)
- Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602)
- TLS certificate verification in the `codepipeline_project_repo_private` check, which previously used an unverified SSL context, leaving the repository-visibility probe open to MITM tampering [(#11603)](https://github.com/prowler-cloud/prowler/pull/11603)
- Support for Linode cloud provider, with compute, networking and administration services [(#11633)](https://github.com/prowler-cloud/prowler/pull/11633)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Azure provider, mapping existing Azure checks across the five DORA pillars [(#11551)](https://github.com/prowler-cloud/prowler/pull/11551)
- Rename DORA to DORA_2022_2554 to follow the naming <name>_<version> in compliance frameworks [(#11551)](https://github.com/prowler-cloud/prowler/pull/11551)
- `entra_directory_sync_object_takeover_blocked` check for the M365 provider, verifying that hybrid Entra tenants block cloud object takeover through both soft-match and hard-match directory synchronization [(#11098)](https://github.com/prowler-cloud/prowler/pull/11098)
- `entra_conditional_access_policy_no_deleted_object_references` check for M365 provider [(#11236)](https://github.com/prowler-cloud/prowler/pull/11236)
- `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028)
- `recovery_vault_has_protected_items` check for Azure provider, verifying that Recovery Services vaults have at least one protected backup item [(#11048)](https://github.com/prowler-cloud/prowler/pull/11048)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645)
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646)
- `cloudfront_distributions_pqc_tls_enabled` check for AWS provider to verify CloudFront distributions enforce a post-quantum TLS 1.3 security policy [(#11317)](https://github.com/prowler-cloud/prowler/pull/11317)
- `apigateway_domain_name_pqc_tls_enabled` check for AWS provider to verify API Gateway custom domain names use a post-quantum TLS security policy [(#11316)](https://github.com/prowler-cloud/prowler/pull/11316)
- `transfer_server_pqc_ssh_kex_enabled` check for AWS provider to verify Transfer Family servers use a post-quantum hybrid SSH key exchange security policy [(#11315)](https://github.com/prowler-cloud/prowler/pull/11315)
- `acmpca_certificate_authority_pqc_key_algorithm` check and new `acmpca` service for AWS provider to verify AWS Private CA certificate authorities use a post-quantum (ML-DSA) key algorithm [(#11318)](https://github.com/prowler-cloud/prowler/pull/11318)
- `rolesanywhere_trust_anchor_pqc_pki` check and new `rolesanywhere` service for AWS provider to verify IAM Roles Anywhere trust anchors are backed by a post-quantum (ML-DSA) PKI [(#11319)](https://github.com/prowler-cloud/prowler/pull/11319)
- Kubernetes core checks for container CPU limits, CPU requests, memory limits, memory requests, fixed image tags, liveness probes, and readiness probes [(#11373)](https://github.com/prowler-cloud/prowler/pull/11373)
- `recovery_vault_backup_policy_retention_adequate` check for Azure provider, verifying Recovery Services backup policies retain daily backups for at least 30 days [(#11047)](https://github.com/prowler-cloud/prowler/pull/11047)
### 🔄 Changed
- Replaced the unmaintained `awsipranges` dependency with a small standard-library helper for the `route53_dangling_ip_subdomain_takeover` check [(#9293)](https://github.com/prowler-cloud/prowler/pull/9293)
### 🐞 Fixed
- Azure PostgreSQL flexible server inventory no longer aborts the whole subscription when the `connection_throttle.enable` parameter is missing (e.g. PostgreSQL v18), and logs the expected "Entra ID authentication not enabled" case as a warning instead of an error, so servers are still scanned [(#11045)](https://github.com/prowler-cloud/prowler/pull/11045)
- `iam_policy_allows_privilege_escalation` now includes the `privilege-escalation` category [(#11648)](https://github.com/prowler-cloud/prowler/pull/11648)
### 🔐 Security
- `pytest` from 8.3.5 to 9.0.3, patching a known vulnerability in the SDK test dependency [(#11291)](https://github.com/prowler-cloud/prowler/pull/11291)
- `black` from 25.1.0 to 26.3.1, patching a known vulnerability in the SDK formatter dependency [(#11290)](https://github.com/prowler-cloud/prowler/pull/11290)
- `microsoft-kiota-*` to 1.9.9 and `aiohttp` to 3.14.0, patching known CVEs [(#11596)](https://github.com/prowler-cloud/prowler/pull/11596)
- Container base image bumped to `python:3.12.13-slim-bookworm` (patches `libgnutls30` CVE-2026-33845 and CVE-2026-42010) and `trivy` bumped to 0.71.0 (patches embedded `golang.org/x/crypto` and Go stdlib CVEs); `.trivyignore` documents remaining bookworm criticals with no-fix or not-affected rationale [(#11592)](https://github.com/prowler-cloud/prowler/pull/11592)
---
## [5.30.3] (Prowler v5.30.3)
### 🐞 Fixed
@@ -231,7 +151,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788)
- `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000)
- `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858)
- Per-provider scan configuration schema with bounds validation that drops out-of-range values with a warning on config load [(#11518)](https://github.com/prowler-cloud/prowler/pull/11518)
- Okta provider with OAuth 2.0 authentication and `signon_global_session_idle_timeout_15min` check [(#11079)](https://github.com/prowler-cloud/prowler/pull/11079)
- `sagemaker_domain_sso_configured` check for AWS provider [(#11094)](https://github.com/prowler-cloud/prowler/pull/11094)
- Scaleway provider with `iam_api_keys_no_root_owned` check [(#11166)](https://github.com/prowler-cloud/prowler/pull/11166)
-5
View File
@@ -147,7 +147,6 @@ from prowler.providers.iac.models import IACOutputOptions
from prowler.providers.image.exceptions.exceptions import ImageBaseException
from prowler.providers.image.models import ImageOutputOptions
from prowler.providers.kubernetes.models import KubernetesOutputOptions
from prowler.providers.linode.models import LinodeOutputOptions
from prowler.providers.llm.models import LLMOutputOptions
from prowler.providers.m365.models import M365OutputOptions
from prowler.providers.mongodbatlas.models import MongoDBAtlasOutputOptions
@@ -440,10 +439,6 @@ def prowler():
output_options = ScalewayOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "linode":
output_options = LinodeOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
else:
# Dynamic fallback: any external/custom provider
try:
@@ -1151,9 +1151,6 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
"s3_bucket_secure_transport_policy"
]
-3
View File
@@ -49,9 +49,6 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_ssl_listeners",
"elbv2_nlb_tls_termination_enabled",
"s3_bucket_secure_transport_policy",
+2 -8
View File
@@ -2366,10 +2366,7 @@
}
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
"elbv2_insecure_ssl_ciphers"
]
},
{
@@ -2392,10 +2389,7 @@
}
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
"elbv2_insecure_ssl_ciphers"
]
},
{
@@ -1145,9 +1145,6 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
@@ -1167,9 +1164,6 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
-3
View File
@@ -487,9 +487,6 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"s3_bucket_secure_transport_policy"
]
@@ -266,9 +266,6 @@
"ec2_ebs_default_encryption",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_encryption_at_rest_enabled",
"opensearch_service_domains_node_to_node_encryption_enabled",
@@ -35,10 +35,7 @@
],
"Checks": [
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
"elbv2_insecure_ssl_ciphers"
]
},
{
@@ -2040,9 +2040,6 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
"elbv2_ssl_listeners",
"glue_data_catalogs_connection_passwords_encryption_enabled",
@@ -3093,9 +3090,6 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
"elbv2_listeners_underneath",
"elbv2_logging_enabled",
@@ -2042,9 +2042,6 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_nlb_tls_termination_enabled",
"elbv2_ssl_listeners",
"glue_data_catalogs_connection_passwords_encryption_enabled",
@@ -3096,9 +3093,6 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elbv2_internet_facing",
"elbv2_listeners_underneath",
"elbv2_logging_enabled",
@@ -653,9 +653,6 @@
"apigateway_restapi_client_certificate_enabled",
"ec2_ebs_volume_encryption",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy"
@@ -5262,9 +5262,6 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"opensearch_service_domains_node_to_node_encryption_enabled",
"s3_bucket_secure_transport_policy"
@@ -5552,9 +5549,6 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners"
]
},
@@ -40,9 +40,6 @@
"ec2_instance_public_ip",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"elb_ssl_listeners",
"ec2_ebs_default_encryption",
"emr_cluster_master_nodes_no_public_ip",
@@ -474,9 +474,6 @@
"elbv2_ssl_listeners",
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
"redshift_cluster_in_transit_encryption_enabled",
"elasticache_redis_cluster_in_transit_encryption_enabled",
"dynamodb_accelerator_cluster_in_transit_encryption_enabled",

Some files were not shown because too many files have changed in this diff Show More