mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
chore(ui): resolve master merge conflicts
This commit is contained in:
@@ -6,14 +6,20 @@
|
||||
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
|
||||
# Google Tag Manager ID (empty/unset ⇒ GTM not loaded, zero egress)
|
||||
# deprecated, use UI_GOOGLE_TAG_MANAGER_ID
|
||||
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
|
||||
UI_GOOGLE_TAG_MANAGER_ID=""
|
||||
|
||||
#### MCP Server ####
|
||||
PROWLER_MCP_VERSION=stable
|
||||
@@ -139,13 +145,19 @@ DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
DJANGO_SENTRY_DSN=
|
||||
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
|
||||
|
||||
# Sentry settings
|
||||
SENTRY_ENVIRONMENT=local
|
||||
# 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_RELEASE=local
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
# Reserved runtime public config (registered now; no UI consumer yet)
|
||||
# POSTHOG_KEY=
|
||||
# POSTHOG_HOST=
|
||||
# REO_DEV_CLIENT_ID=
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.31.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.32.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -77,6 +77,11 @@ 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/*"
|
||||
|
||||
@@ -590,6 +590,30 @@ jobs:
|
||||
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'
|
||||
|
||||
@@ -32,9 +32,6 @@ 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:
|
||||
@@ -146,7 +143,6 @@ 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: |
|
||||
|
||||
@@ -40,7 +40,8 @@ jobs:
|
||||
AUTH_SECRET: 'fallback-ci-secret-for-testing'
|
||||
AUTH_TRUST_HOST: true
|
||||
NEXTAUTH_URL: 'http://localhost:3000'
|
||||
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
|
||||
AUTH_URL: 'http://localhost:3000'
|
||||
UI_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 }}
|
||||
@@ -165,7 +166,7 @@ jobs:
|
||||
timeout=150
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if curl -s ${NEXT_PUBLIC_API_BASE_URL}/docs >/dev/null 2>&1; then
|
||||
if curl -s ${UI_API_BASE_URL}/docs >/dev/null 2>&1; then
|
||||
echo "Prowler API is ready!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
.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 && \
|
||||
|
||||
@@ -104,23 +104,24 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AWS | 600 | 84 | 44 | 18 | Official | UI, API, CLI |
|
||||
| Azure | 167 | 22 | 19 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 102 | 18 | 17 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 83 | 7 | 7 | 11 | Official | UI, API, CLI |
|
||||
| AWS | 613 | 86 | 46 | 19 | Official | UI, API, CLI |
|
||||
| Azure | 190 | 22 | 20 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 109 | 20 | 18 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 90 | 7 | 7 | 11 | Official | UI, API, CLI |
|
||||
| GitHub | 24 | 3 | 1 | 5 | Official | UI, API, CLI |
|
||||
| M365 | 102 | 10 | 4 | 10 | Official | UI, API, CLI |
|
||||
| OCI | 51 | 14 | 4 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 9 | 4 | 9 | Official | UI, API, CLI |
|
||||
| Cloudflare | 29 | 3 | 0 | 5 | Official | UI, API, CLI |
|
||||
| M365 | 107 | 10 | 4 | 10 | Official | UI, API, CLI |
|
||||
| OCI | 52 | 14 | 4 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 9 | 5 | 9 | Official | UI, API, CLI |
|
||||
| Cloudflare | 29 | 3 | 1 | 5 | Official | UI, API, CLI |
|
||||
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
|
||||
| MongoDB Atlas | 10 | 3 | 0 | 8 | Official | UI, API, CLI |
|
||||
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
|
||||
| Image | N/A | N/A | N/A | N/A | Official | CLI, API |
|
||||
| Google Workspace | 39 | 5 | 2 | 5 | Official | UI, API, CLI |
|
||||
| Google Workspace | 65 | 11 | 2 | 6 | Official | UI, API, CLI |
|
||||
| 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 |
|
||||
| Okta | 29 | 8 | 1 | 2 | Official | UI, API, 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 |
|
||||
|
||||
@@ -24,6 +24,9 @@ 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=
|
||||
|
||||
|
||||
+16
-1
@@ -2,18 +2,25 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.32.0] (Prowler UNRELEASED)
|
||||
## [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)
|
||||
|
||||
### 🐞 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
|
||||
|
||||
@@ -23,6 +30,14 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [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)
|
||||
|
||||
---
|
||||
|
||||
## [1.31.2] (Prowler v5.30.2)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -196,6 +196,42 @@ 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`.
|
||||
|
||||
@@ -65,6 +65,7 @@ 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. |
|
||||
|
||||
+4
-2
@@ -62,7 +62,8 @@ dependencies = [
|
||||
"gevent (==25.9.1)",
|
||||
"werkzeug (==3.1.7)",
|
||||
"sqlparse (==0.5.5)",
|
||||
"fonttools (==4.62.1)"
|
||||
"fonttools (==4.62.1)",
|
||||
"uvicorn-worker (==0.4.0)",
|
||||
]
|
||||
description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
@@ -70,7 +71,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.32.0"
|
||||
version = "1.33.0"
|
||||
|
||||
[tool.uv]
|
||||
# Transitive pins matching master to avoid silent drift; bump deliberately.
|
||||
@@ -422,6 +423,7 @@ 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",
|
||||
|
||||
@@ -3,7 +3,14 @@ from django.db import transaction
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Membership, Role, Tenant, User, UserRoleRelationship
|
||||
from api.models import (
|
||||
Membership,
|
||||
Role,
|
||||
SAMLConfiguration,
|
||||
Tenant,
|
||||
User,
|
||||
UserRoleRelationship,
|
||||
)
|
||||
|
||||
|
||||
class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
@@ -18,7 +25,42 @@ 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
|
||||
# existing user is already a member of that tenant.
|
||||
email = sociallogin.user.email
|
||||
if not email:
|
||||
return
|
||||
|
||||
domain = email.rsplit("@", 1)[-1].lower()
|
||||
resolver_match = getattr(request, "resolver_match", None)
|
||||
organization_slug = (
|
||||
(resolver_match.kwargs or {}).get("organization_slug", "")
|
||||
if resolver_match
|
||||
else ""
|
||||
).lower()
|
||||
# The ACS endpoint is scoped per email domain; reject mismatches so an
|
||||
# attacker cannot replay an assertion through another tenant's endpoint.
|
||||
if organization_slug != domain:
|
||||
return
|
||||
|
||||
try:
|
||||
saml_config = SAMLConfiguration.objects.using(MainRouter.admin_db).get(
|
||||
email_domain=domain
|
||||
)
|
||||
except SAMLConfiguration.DoesNotExist:
|
||||
return
|
||||
|
||||
existing_user = self.get_user_by_email(email)
|
||||
if existing_user and existing_user.is_member_of_tenant(
|
||||
str(saml_config.tenant_id)
|
||||
):
|
||||
sociallogin.connect(request, existing_user)
|
||||
return
|
||||
|
||||
if email:
|
||||
existing_user = self.get_user_by_email(email)
|
||||
if existing_user:
|
||||
|
||||
@@ -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``, ``csa_ccm_4.0``).
|
||||
(e.g. ``dora_2022_2554``, ``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").
|
||||
list[str]: Framework identifiers (e.g., "cis_1.4_aws", "dora_2022_2554").
|
||||
"""
|
||||
global AVAILABLE_COMPLIANCE_FRAMEWORKS
|
||||
if provider_type not in AVAILABLE_COMPLIANCE_FRAMEWORKS:
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
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,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.32.0
|
||||
version: 1.33.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -5,9 +6,48 @@ from allauth.socialaccount.models import SocialLogin
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from api.adapters import ProwlerSocialAccountAdapter
|
||||
from api.db_router import MainRouter
|
||||
from api.models import SAMLConfiguration
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Minimal, well-formed IdP metadata accepted by SAMLConfiguration._parse_metadata.
|
||||
VALID_METADATA = """<?xml version='1.0' encoding='UTF-8'?>
|
||||
<md:EntityDescriptor entityID='TEST' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'>
|
||||
<md:IDPSSODescriptor WantAuthnRequestsSigned='false' protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol'>
|
||||
<md:KeyDescriptor use='signing'>
|
||||
<ds:KeyInfo xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>FAKECERTDATA</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleSignOnService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' Location='https://idp.test/sso'/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
"""
|
||||
|
||||
|
||||
def _saml_request(rf, organization_slug):
|
||||
"""Build an ACS request whose resolver_match carries the organization slug,
|
||||
mirroring how Django populates it after routing the SAML ACS URL."""
|
||||
request = rf.post(f"/api/v1/accounts/saml/{organization_slug}/acs/finish/")
|
||||
request.resolver_match = SimpleNamespace(
|
||||
kwargs={"organization_slug": organization_slug}
|
||||
)
|
||||
return request
|
||||
|
||||
|
||||
def _saml_sociallogin(user):
|
||||
sociallogin = MagicMock(spec=SocialLogin)
|
||||
sociallogin.account = MagicMock()
|
||||
sociallogin.provider = MagicMock()
|
||||
sociallogin.provider.id = "saml"
|
||||
sociallogin.account.extra_data = {}
|
||||
sociallogin.user = user
|
||||
sociallogin.connect = MagicMock()
|
||||
return sociallogin
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProwlerSocialAccountAdapter:
|
||||
@@ -20,26 +60,99 @@ class TestProwlerSocialAccountAdapter:
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
assert adapter.get_user_by_email("notfound@example.com") is None
|
||||
|
||||
def test_pre_social_login_links_existing_user(self, create_test_user, rf):
|
||||
def test_pre_social_login_links_member_of_saml_tenant(
|
||||
self, create_test_user, tenants_fixture, rf
|
||||
):
|
||||
"""A SAML login links to an existing account only when that user is
|
||||
already a member of the tenant that owns the asserted email domain."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
# create_test_user (dev@prowler.com) is a member of tenant1.
|
||||
domain = create_test_user.email.rsplit("@", 1)[-1]
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
email_domain=domain,
|
||||
metadata_xml=VALID_METADATA,
|
||||
tenant=tenants_fixture[0],
|
||||
)
|
||||
|
||||
sociallogin = MagicMock(spec=SocialLogin)
|
||||
sociallogin.account = MagicMock()
|
||||
sociallogin.provider = MagicMock()
|
||||
sociallogin.provider.id = "saml"
|
||||
sociallogin.account.extra_data = {}
|
||||
sociallogin.user = create_test_user
|
||||
sociallogin.connect = MagicMock()
|
||||
|
||||
adapter.pre_social_login(rf.get("/"), sociallogin)
|
||||
sociallogin = _saml_sociallogin(create_test_user)
|
||||
adapter.pre_social_login(_saml_request(rf, domain), sociallogin)
|
||||
|
||||
call_args = sociallogin.connect.call_args
|
||||
assert call_args is not None
|
||||
|
||||
called_request, called_user = call_args[0]
|
||||
assert called_request.path == "/"
|
||||
_, called_user = call_args[0]
|
||||
assert called_user.email == create_test_user.email
|
||||
|
||||
def test_pre_social_login_blocks_cross_tenant_takeover(
|
||||
self, create_test_user, tenants_fixture, rf
|
||||
):
|
||||
"""GHSA-h8m9-jgf8-vwvp: an attacker tenant that claims the victim's
|
||||
email domain must NOT be able to link to the victim's account, because
|
||||
the victim is not a member of the attacker's tenant."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
domain = create_test_user.email.rsplit("@", 1)[-1]
|
||||
# tenant3 is the attacker tenant; create_test_user is NOT a member of it.
|
||||
attacker_tenant = tenants_fixture[2]
|
||||
assert not create_test_user.is_member_of_tenant(str(attacker_tenant.id))
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
email_domain=domain,
|
||||
metadata_xml=VALID_METADATA,
|
||||
tenant=attacker_tenant,
|
||||
)
|
||||
|
||||
sociallogin = _saml_sociallogin(create_test_user)
|
||||
adapter.pre_social_login(_saml_request(rf, domain), sociallogin)
|
||||
|
||||
sociallogin.connect.assert_not_called()
|
||||
|
||||
def test_pre_social_login_blocks_domain_slug_mismatch(
|
||||
self, create_test_user, tenants_fixture, rf
|
||||
):
|
||||
"""The asserted email domain must match the ACS endpoint's slug, so an
|
||||
assertion cannot be replayed through a different tenant's endpoint."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
domain = create_test_user.email.rsplit("@", 1)[-1]
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
email_domain=domain,
|
||||
metadata_xml=VALID_METADATA,
|
||||
tenant=tenants_fixture[0],
|
||||
)
|
||||
|
||||
sociallogin = _saml_sociallogin(create_test_user)
|
||||
# Slug points at a different domain than the asserted email.
|
||||
adapter.pre_social_login(_saml_request(rf, "attacker.com"), sociallogin)
|
||||
|
||||
sociallogin.connect.assert_not_called()
|
||||
|
||||
def test_pre_social_login_blocks_when_no_saml_config(
|
||||
self, create_test_user, tenants_fixture, rf
|
||||
):
|
||||
"""No SAML configuration for the domain means nothing to link against."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
domain = create_test_user.email.rsplit("@", 1)[-1]
|
||||
|
||||
sociallogin = _saml_sociallogin(create_test_user)
|
||||
adapter.pre_social_login(_saml_request(rf, domain), sociallogin)
|
||||
|
||||
sociallogin.connect.assert_not_called()
|
||||
|
||||
def test_pre_social_login_blocks_without_resolver_match(
|
||||
self, create_test_user, tenants_fixture, rf
|
||||
):
|
||||
"""Fail closed: if the request has no resolver_match we cannot bind the
|
||||
assertion to a tenant, so no linking happens."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
domain = create_test_user.email.rsplit("@", 1)[-1]
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
email_domain=domain,
|
||||
metadata_xml=VALID_METADATA,
|
||||
tenant=tenants_fixture[0],
|
||||
)
|
||||
|
||||
sociallogin = _saml_sociallogin(create_test_user)
|
||||
adapter.pre_social_login(rf.post("/"), sociallogin)
|
||||
|
||||
sociallogin.connect.assert_not_called()
|
||||
|
||||
def test_pre_social_login_no_link_if_email_missing(self, rf):
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
|
||||
@@ -47,14 +160,35 @@ class TestProwlerSocialAccountAdapter:
|
||||
sociallogin.account = MagicMock()
|
||||
sociallogin.provider = MagicMock()
|
||||
sociallogin.user = MagicMock()
|
||||
sociallogin.user.email = ""
|
||||
sociallogin.provider.id = "saml"
|
||||
sociallogin.account.extra_data = {}
|
||||
sociallogin.connect = MagicMock()
|
||||
|
||||
adapter.pre_social_login(rf.get("/"), sociallogin)
|
||||
adapter.pre_social_login(_saml_request(rf, "prowler.com"), sociallogin)
|
||||
|
||||
sociallogin.connect.assert_not_called()
|
||||
|
||||
def test_pre_social_login_non_saml_links_by_email(self, create_test_user, rf):
|
||||
"""Non-SAML providers (e.g. Google/GitHub) still link to an existing
|
||||
local account by email; the tenant binding only applies to SAML."""
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
|
||||
sociallogin = MagicMock(spec=SocialLogin)
|
||||
sociallogin.account = MagicMock()
|
||||
sociallogin.provider = MagicMock()
|
||||
sociallogin.provider.id = "google"
|
||||
sociallogin.account.extra_data = {"email": create_test_user.email}
|
||||
sociallogin.user = create_test_user
|
||||
sociallogin.connect = MagicMock()
|
||||
|
||||
adapter.pre_social_login(rf.get("/"), sociallogin)
|
||||
|
||||
call_args = sociallogin.connect.call_args
|
||||
assert call_args is not None
|
||||
_, called_user = call_args[0]
|
||||
assert called_user.email == create_test_user.email
|
||||
|
||||
def test_save_user_saml_sets_session_flag(self, rf):
|
||||
adapter = ProwlerSocialAccountAdapter()
|
||||
request = rf.get("/")
|
||||
|
||||
@@ -41,3 +41,30 @@ 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()
|
||||
|
||||
@@ -13586,7 +13586,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -13606,18 +13608,23 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(
|
||||
tenant_id=tenants_fixture[0].id
|
||||
)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenants_fixture[0]
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -13665,6 +13672,81 @@ class TestTenantFinishACSView:
|
||||
user.company_name = original_company
|
||||
user.save()
|
||||
|
||||
def test_dispatch_rejects_assertion_email_domain_that_differs_from_slug(
|
||||
self, tenants_fixture, saml_setup, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("AUTH_URL", "http://localhost")
|
||||
monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete")
|
||||
victim_tenant = tenants_fixture[0]
|
||||
attacker_tenant = tenants_fixture[1]
|
||||
attacker_domain = "attacker.com"
|
||||
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
email_domain=attacker_domain,
|
||||
metadata_xml="""<?xml version='1.0' encoding='UTF-8'?>
|
||||
<md:EntityDescriptor entityID='ATTACKER' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'>
|
||||
<md:IDPSSODescriptor WantAuthnRequestsSigned='false' protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol'>
|
||||
<md:KeyDescriptor use='signing'>
|
||||
<ds:KeyInfo xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>TEST</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:SingleSignOnService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' Location='https://ATTACKER/sso/saml'/>
|
||||
</md:IDPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
""",
|
||||
tenant=attacker_tenant,
|
||||
)
|
||||
user = User.objects.using(MainRouter.admin_db).create(
|
||||
email=f"intruder@{saml_setup['domain']}", name="Intruder"
|
||||
)
|
||||
social_account = SocialAccount(
|
||||
user=user,
|
||||
provider="ATTACKER",
|
||||
extra_data={
|
||||
"firstName": ["Mallory"],
|
||||
"lastName": ["Example"],
|
||||
},
|
||||
)
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": attacker_domain})
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"allauth.socialaccount.providers.saml.views.get_app_or_404"
|
||||
) as mock_get_app_or_404,
|
||||
patch(
|
||||
"allauth.socialaccount.models.SocialAccount.objects.get"
|
||||
) as mock_sa_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml",
|
||||
provider_id="ATTACKER",
|
||||
client_id=attacker_domain,
|
||||
name="Attacker App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug=attacker_domain)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert "sso_saml_failed=true" in response.url
|
||||
assert not (
|
||||
Membership.objects.using(MainRouter.admin_db)
|
||||
.filter(user=user, tenant=victim_tenant)
|
||||
.exists()
|
||||
)
|
||||
assert (
|
||||
not SAMLToken.objects.using(MainRouter.admin_db).filter(user=user).exists()
|
||||
)
|
||||
|
||||
def test_rollback_saml_user_when_error_occurs(self, users_fixture, monkeypatch):
|
||||
"""Test that a user is properly deleted when created during SAML flow and an error occurs"""
|
||||
monkeypatch.setenv("AUTH_URL", "http://localhost")
|
||||
@@ -13734,7 +13816,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -13754,16 +13838,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -13802,7 +13891,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -13822,16 +13913,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -13881,7 +13977,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -13901,16 +13999,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -13959,7 +14062,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -13979,16 +14084,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -14043,7 +14153,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = user
|
||||
request.session = {}
|
||||
@@ -14063,16 +14175,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
@@ -14126,7 +14243,9 @@ class TestTenantFinishACSView:
|
||||
)
|
||||
|
||||
request = RequestFactory().get(
|
||||
reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"})
|
||||
reverse(
|
||||
"saml_finish_acs", kwargs={"organization_slug": saml_setup["domain"]}
|
||||
)
|
||||
)
|
||||
request.user = non_admin_user
|
||||
request.session = {}
|
||||
@@ -14146,16 +14265,21 @@ class TestTenantFinishACSView:
|
||||
patch("api.models.User.objects.get") as mock_user_get,
|
||||
):
|
||||
mock_get_app_or_404.return_value = MagicMock(
|
||||
provider="saml", client_id="testtenant", name="Test App", settings={}
|
||||
provider="saml",
|
||||
client_id=saml_setup["domain"],
|
||||
name="Test App",
|
||||
settings={},
|
||||
)
|
||||
mock_sa_get.return_value = social_account
|
||||
mock_socialapp_get.return_value = MagicMock(provider_id="saml")
|
||||
mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id)
|
||||
mock_saml_config_get.return_value = MagicMock()
|
||||
mock_saml_config_get.return_value = SimpleNamespace(
|
||||
email_domain=saml_setup["domain"], tenant=tenant
|
||||
)
|
||||
mock_user_get.return_value = non_admin_user
|
||||
|
||||
view = TenantFinishACSView.as_view()
|
||||
response = view(request, organization_slug="testtenant")
|
||||
response = view(request, organization_slug=saml_setup["domain"])
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
@@ -767,7 +767,10 @@ class TenantFinishACSView(FinishACSView):
|
||||
try:
|
||||
check = SAMLDomainIndex.objects.get(email_domain=organization_slug)
|
||||
with rls_transaction(str(check.tenant_id)):
|
||||
SAMLConfiguration.objects.get(tenant_id=str(check.tenant_id))
|
||||
saml_config = SAMLConfiguration.objects.select_related("tenant").get(
|
||||
tenant_id=str(check.tenant_id)
|
||||
)
|
||||
tenant = saml_config.tenant
|
||||
social_app = SocialApp.objects.get(
|
||||
provider="saml", client_id=organization_slug
|
||||
)
|
||||
@@ -787,6 +790,15 @@ class TenantFinishACSView(FinishACSView):
|
||||
callback_url = env.str("AUTH_URL")
|
||||
return redirect(f"{callback_url}?sso_saml_failed=true")
|
||||
|
||||
requested_domain = organization_slug.lower()
|
||||
configured_domain = saml_config.email_domain.lower()
|
||||
email_domain = user.email.rsplit("@", 1)[-1].lower()
|
||||
if configured_domain != requested_domain or email_domain != configured_domain:
|
||||
logger.error("SAML email domain does not match requested organization")
|
||||
self._rollback_saml_user(request)
|
||||
callback_url = env.str("AUTH_URL")
|
||||
return redirect(f"{callback_url}?sso_saml_failed=true")
|
||||
|
||||
extra = social_account.extra_data
|
||||
user.first_name = (
|
||||
extra.get("firstName", [""])[0] if extra.get("firstName") else ""
|
||||
@@ -800,13 +812,6 @@ class TenantFinishACSView(FinishACSView):
|
||||
user.name = "N/A"
|
||||
user.save()
|
||||
|
||||
email_domain = user.email.split("@")[-1]
|
||||
tenant = (
|
||||
SAMLConfiguration.objects.using(MainRouter.admin_db)
|
||||
.get(email_domain=email_domain)
|
||||
.tenant
|
||||
)
|
||||
|
||||
# Only remap roles when the IdP provides a userType attribute.
|
||||
# Without it, the user's current roles are left untouched.
|
||||
role_name = (
|
||||
@@ -1884,7 +1889,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' and 'csa_ccm_4.0'); any "
|
||||
"produce this artifact (currently 'dora_2022_2554' and 'csa_ccm_4.0'); any "
|
||||
"other framework returns 404."
|
||||
),
|
||||
parameters=[
|
||||
@@ -1893,7 +1898,7 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
|
||||
type=str,
|
||||
location=OpenApiParameter.PATH,
|
||||
required=True,
|
||||
description="The compliance report name, like 'dora'",
|
||||
description="The compliance report name, like 'dora_2022_2554'",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
|
||||
@@ -49,6 +49,7 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"api.middleware.CloseDBConnectionsMiddleware",
|
||||
"django_guid.middleware.guid_middleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
|
||||
@@ -3,6 +3,8 @@ 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
|
||||
@@ -12,6 +14,7 @@ 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
|
||||
@@ -19,34 +22,28 @@ 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
|
||||
|
||||
# Native ASGI worker (gunicorn 24+). Required so SSE endpoints can keep the
|
||||
# event loop alive while waiting for events.
|
||||
worker_class = env("DJANGO_WORKER_CLASS", default="asgi")
|
||||
|
||||
# Lifespan protocol. Django's ASGIHandler (config.asgi:application) serves only
|
||||
# HTTP scopes and raises "Django can only handle ASGI/HTTP connections, not
|
||||
# lifespan." gunicorn's default ("auto") probes the app with a lifespan scope
|
||||
# to detect support, which triggers that error. We use no lifespan startup or
|
||||
# shutdown hooks, so disable the protocol entirely.
|
||||
asgi_lifespan = env("DJANGO_ASGI_LIFESPAN", default="off")
|
||||
|
||||
# Event loop for the ASGI worker. "auto" uses uvloop when it is installed and
|
||||
# falls back to the stdlib asyncio loop otherwise; uvloop gives the SSE event
|
||||
# loop more headroom under many concurrent open streams.
|
||||
asgi_loop = env("DJANGO_ASGI_LOOP", default="uvloop")
|
||||
|
||||
# Max concurrent connections per ASGI worker. Each open SSE stream holds one
|
||||
# connection for its whole lifetime, so this caps simultaneous SSE clients per
|
||||
# worker (gunicorn's default is 1000). The sync-only `threads` option has no
|
||||
# effect on ASGI workers.
|
||||
worker_connections = env.int("DJANGO_WORKER_CONNECTIONS", default=1000)
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -53,3 +53,8 @@ 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")
|
||||
|
||||
@@ -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.json`) are emitted
|
||||
# Universal-only frameworks (top-level JSONs like `dora_2022_2554.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.json`).
|
||||
# Universal-only frameworks (e.g. `dora_2022_2554.json`).
|
||||
if universal_only_names:
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks=universal_only_names,
|
||||
|
||||
Generated
+30
-1
@@ -357,6 +357,7 @@ 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" },
|
||||
@@ -4535,7 +4536,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.32.0"
|
||||
version = "1.33.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
@@ -4575,6 +4576,7 @@ dependencies = [
|
||||
{ name = "sentry-sdk", extra = ["django"] },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "uuid6" },
|
||||
{ name = "uvicorn-worker" },
|
||||
{ name = "uvloop" },
|
||||
{ name = "werkzeug" },
|
||||
{ name = "xmlsec" },
|
||||
@@ -4641,6 +4643,7 @@ requires-dist = [
|
||||
{ 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" },
|
||||
@@ -5846,6 +5849,32 @@ 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"
|
||||
|
||||
@@ -438,6 +438,34 @@ 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,8 +11,7 @@ data:
|
||||
{{- else }}
|
||||
AUTH_URL: {{ .Values.ui.authUrl | quote }}
|
||||
{{- end }}
|
||||
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"
|
||||
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"
|
||||
AUTH_TRUST_HOST: "true"
|
||||
UI_PORT: {{ .Values.ui.service.port | quote }}
|
||||
|
||||
@@ -21,8 +21,8 @@ fullnameOverride: ""
|
||||
|
||||
secrets:
|
||||
SITE_URL: http://localhost:3000
|
||||
API_BASE_URL: http://prowler-api:8080/api/v1
|
||||
NEXT_PUBLIC_API_DOCS_URL: http://prowler-api:8080/api/v1/docs
|
||||
UI_API_BASE_URL: http://prowler-api:8080/api/v1
|
||||
UI_API_DOCS_URL: http://prowler-api:8080/api/v1/docs
|
||||
AUTH_TRUST_HOST: True
|
||||
UI_PORT: 3000
|
||||
# openssl rand -base64 32
|
||||
|
||||
@@ -40,9 +40,181 @@ 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.
|
||||
|
||||
@@ -221,9 +221,9 @@ Before running E2E tests:
|
||||
```
|
||||
|
||||
- **Ensure Prowler API is available**
|
||||
- By default, Playwright uses `NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1` (configured in `playwright.config.ts`).
|
||||
- By default, Playwright uses `UI_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 `NEXT_PUBLIC_API_BASE_URL` accordingly before running the tests.
|
||||
- If a different API URL is required, set `UI_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).
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
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).
|
||||
@@ -108,6 +108,39 @@ 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.json`, `prowler/compliance/csa_ccm_4.0.json`.
|
||||
- Universal: `prowler/compliance/dora_2022_2554.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.json`.
|
||||
Examples in the repository: `prowler/compliance/csa_ccm_4.0.json`, `prowler/compliance/dora_2022_2554.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.json` → `dora`).
|
||||
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`).
|
||||
|
||||
### 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.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_2022_2554.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.json` that key is `dora`, 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_2022_2554.json` that key is `dora_2022_2554`, 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.json` — universal schema, single-provider populated (AWS), ready to extend with more providers.
|
||||
- `prowler/compliance/dora_2022_2554.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.
|
||||
|
||||
@@ -359,6 +359,13 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -417,6 +424,7 @@
|
||||
"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.31.0"
|
||||
PROWLER_API_VERSION="5.31.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -32,6 +32,7 @@ Prowler supports a wide range of providers organized by category:
|
||||
| [Azure](/user-guide/providers/azure/getting-started-azure) | Official | Subscriptions | UI, API, CLI |
|
||||
| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI |
|
||||
| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI |
|
||||
| [Linode](/user-guide/providers/linode/getting-started-linode) | [Contact us](https://prowler.com/contact) | Accounts | CLI |
|
||||
| **NHN** | [Contact us](https://prowler.com/contact) | Tenants | CLI |
|
||||
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI |
|
||||
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI |
|
||||
|
||||
+49
-20
@@ -2,6 +2,8 @@
|
||||
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.
|
||||
@@ -81,6 +83,39 @@ 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.
|
||||
@@ -201,35 +236,29 @@ When running Prowler behind a reverse proxy (nginx, Traefik, etc.) or load balan
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
**Solution:**
|
||||
|
||||
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:
|
||||
Set the runtime environment variables to your external URL and restart the UI container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ui:
|
||||
image: prowler-ui-custom:latest # Use your custom-built image
|
||||
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
|
||||
# ... 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>
|
||||
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.
|
||||
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.
|
||||
</Note>
|
||||
|
||||
@@ -10,14 +10,20 @@ 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 |
|
||||
@@ -55,6 +61,9 @@ 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 |
|
||||
@@ -67,6 +76,7 @@ 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 |
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
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 |
|
||||
@@ -36,6 +36,8 @@
|
||||
"lightsail:GetRelationalDatabases",
|
||||
"macie2:GetMacieSession",
|
||||
"macie2:GetAutomatedDiscoveryConfiguration",
|
||||
"rolesanywhere:ListTagsForResource",
|
||||
"rolesanywhere:ListTrustAnchors",
|
||||
"s3:GetAccountPublicAccessBlock",
|
||||
"shield:DescribeProtection",
|
||||
"shield:GetSubscriptionState",
|
||||
@@ -61,7 +63,9 @@
|
||||
],
|
||||
"Resource": [
|
||||
"arn:*:apigateway:*::/restapis/*",
|
||||
"arn:*:apigateway:*::/apis/*"
|
||||
"arn:*:apigateway:*::/apis/*",
|
||||
"arn:*:apigateway:*::/domainnames",
|
||||
"arn:*:apigateway:*::/domainnames/*"
|
||||
],
|
||||
"Sid": "AllowAPIGatewayReadOnly"
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ Resources:
|
||||
- "lightsail:GetRelationalDatabases"
|
||||
- "macie2:GetMacieSession"
|
||||
- "macie2:GetAutomatedDiscoveryConfiguration"
|
||||
- "rolesanywhere:ListTagsForResource"
|
||||
- "rolesanywhere:ListTrustAnchors"
|
||||
- "s3:GetAccountPublicAccessBlock"
|
||||
- "shield:DescribeProtection"
|
||||
- "shield:GetSubscriptionState"
|
||||
@@ -150,6 +152,8 @@ Resources:
|
||||
Resource:
|
||||
- "arn:*:apigateway:*::/restapis/*"
|
||||
- "arn:*:apigateway:*::/apis/*"
|
||||
- "arn:*:apigateway:*::/domainnames"
|
||||
- "arn:*:apigateway:*::/domainnames/*"
|
||||
- !If
|
||||
- OrganizationsEnabled
|
||||
- PolicyName: ProwlerOrganizations
|
||||
|
||||
+46
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.31.0] (Prowler UNRELEASED)
|
||||
## [5.31.0] (Prowler v5.31.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -12,23 +12,59 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `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)
|
||||
@@ -38,6 +74,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.30.3] (Prowler v5.30.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- CLI compliance summary tables no longer undercount findings mapped to multiple sections nor double-count a single finding mapped to several requirements within the same group/split, and the Provider column no longer leaks a value from another framework [(#11567)](https://github.com/prowler-cloud/prowler/pull/11567)
|
||||
|
||||
---
|
||||
|
||||
## [5.30.2] (Prowler v5.30.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
@@ -179,6 +223,7 @@ 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)
|
||||
|
||||
@@ -147,6 +147,7 @@ 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
|
||||
@@ -439,6 +440,10 @@ 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,6 +1151,9 @@
|
||||
"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"
|
||||
]
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
"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",
|
||||
|
||||
@@ -2366,7 +2366,10 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"elbv2_insecure_ssl_ciphers"
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"cloudfront_distributions_pqc_tls_enabled",
|
||||
"apigateway_domain_name_pqc_tls_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2389,7 +2392,10 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"elbv2_insecure_ssl_ciphers"
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"cloudfront_distributions_pqc_tls_enabled",
|
||||
"apigateway_domain_name_pqc_tls_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1145,6 +1145,9 @@
|
||||
"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"
|
||||
@@ -1164,6 +1167,9 @@
|
||||
"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"
|
||||
|
||||
@@ -487,6 +487,9 @@
|
||||
"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,6 +266,9 @@
|
||||
"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,7 +35,10 @@
|
||||
],
|
||||
"Checks": [
|
||||
"elb_insecure_ssl_ciphers",
|
||||
"elbv2_insecure_ssl_ciphers"
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"cloudfront_distributions_pqc_tls_enabled",
|
||||
"apigateway_domain_name_pqc_tls_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2040,6 +2040,9 @@
|
||||
"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",
|
||||
@@ -3090,6 +3093,9 @@
|
||||
"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,6 +2042,9 @@
|
||||
"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,6 +3096,9 @@
|
||||
"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,6 +653,9 @@
|
||||
"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,6 +5262,9 @@
|
||||
"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"
|
||||
@@ -5549,6 +5552,9 @@
|
||||
],
|
||||
"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,6 +40,9 @@
|
||||
"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,6 +474,9 @@
|
||||
"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",
|
||||
|
||||
@@ -2714,7 +2714,8 @@
|
||||
"Id": "6.6",
|
||||
"Description": "Ensure that Network Watcher is 'Enabled'",
|
||||
"Checks": [
|
||||
"network_watcher_enabled"
|
||||
"network_watcher_enabled",
|
||||
"network_vnet_ddos_protection_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
@@ -1974,7 +1974,9 @@
|
||||
{
|
||||
"Id": "7.11",
|
||||
"Description": "Ensure subnets are associated with network security groups",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"network_subnet_nsg_associated"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "7 Networking Services",
|
||||
@@ -2094,7 +2096,9 @@
|
||||
{
|
||||
"Id": "8.1.1.1",
|
||||
"Description": "Ensure Microsoft Defender CSPM is set to 'On'",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"defender_ensure_defender_cspm_is_on"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "8 Security Services",
|
||||
@@ -2931,7 +2935,9 @@
|
||||
{
|
||||
"Id": "8.5",
|
||||
"Description": "Ensure Azure DDoS Network Protection is enabled on virtual networks",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"network_vnet_ddos_protection_enabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "8 Security Services",
|
||||
|
||||
@@ -328,6 +328,7 @@
|
||||
"Checks": [
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_user_with_recent_sign_in",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
"iam_custom_role_has_permissions_to_administer_resource_locks",
|
||||
"iam_role_user_access_admin_restricted",
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_user_with_recent_sign_in",
|
||||
"storage_key_rotation_90_days",
|
||||
"keyvault_key_rotation_enabled",
|
||||
"keyvault_rbac_key_expiration_set",
|
||||
@@ -430,6 +431,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"recovery_vault_backup_policy_retention_adequate",
|
||||
"vm_backup_enabled",
|
||||
"vm_sufficient_daily_backup_retention_period",
|
||||
"storage_blob_versioning_is_enabled",
|
||||
@@ -439,7 +441,9 @@
|
||||
"keyvault_recoverable",
|
||||
"sqlserver_auditing_retention_90_days",
|
||||
"postgresql_flexible_server_log_retention_days_greater_3",
|
||||
"cosmosdb_account_backup_policy_continuous"
|
||||
"cosmosdb_account_backup_policy_continuous",
|
||||
"mysql_flexible_server_geo_redundant_backup_enabled",
|
||||
"postgresql_flexible_server_geo_redundant_backup_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"aks_cluster_local_accounts_disabled",
|
||||
"defender_ensure_defender_for_containers_is_on",
|
||||
"defender_ensure_defender_for_cosmosdb_is_on",
|
||||
"defender_ensure_defender_for_databases_is_on",
|
||||
@@ -284,6 +285,7 @@
|
||||
"defender_ensure_notify_alerts_severity_is_high",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_user_with_recent_sign_in",
|
||||
"entra_users_cannot_create_microsoft_365_groups",
|
||||
"iam_custom_role_has_permissions_to_administer_resource_locks",
|
||||
"monitor_alert_create_update_security_solution",
|
||||
@@ -332,7 +334,8 @@
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_policy_user_consent_for_verified_apps"
|
||||
"entra_policy_user_consent_for_verified_apps",
|
||||
"entra_user_with_recent_sign_in"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1102,6 +1105,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_authentication_methods_policy_strong_auth_enforced",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_app",
|
||||
"entra_non_privileged_user_has_mfa entra_privileged_user_has_mfa",
|
||||
"entra_user_with_vm_access_has_mfa",
|
||||
@@ -1267,7 +1271,10 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cosmosdb_account_backup_policy_continuous"
|
||||
"cosmosdb_account_backup_policy_continuous",
|
||||
"mysql_flexible_server_geo_redundant_backup_enabled",
|
||||
"postgresql_flexible_server_geo_redundant_backup_enabled",
|
||||
"recovery_vault_backup_policy_retention_adequate"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1296,7 +1303,11 @@
|
||||
"storage_secure_transfer_required_is_enabled",
|
||||
"vm_ensure_using_managed_disks",
|
||||
"vm_trusted_launch_enabled",
|
||||
"cosmosdb_account_automatic_failover_enabled"
|
||||
"cosmosdb_account_automatic_failover_enabled",
|
||||
"mysql_flexible_server_geo_redundant_backup_enabled",
|
||||
"mysql_flexible_server_high_availability_enabled",
|
||||
"postgresql_flexible_server_geo_redundant_backup_enabled",
|
||||
"postgresql_flexible_server_high_availability_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1340,6 +1351,8 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"aks_cluster_azure_monitor_enabled",
|
||||
"defender_ensure_defender_cspm_is_on",
|
||||
"monitor_alert_create_policy_assignment",
|
||||
"monitor_alert_create_update_nsg",
|
||||
"monitor_alert_create_update_public_ip_address_rule",
|
||||
@@ -1411,6 +1424,7 @@
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"cosmosdb_account_firewall_use_selected_networks",
|
||||
"cosmosdb_account_public_network_access_disabled",
|
||||
"databricks_workspace_no_public_ip_enabled",
|
||||
"databricks_workspace_public_network_access_disabled",
|
||||
"network_bastion_host_exists",
|
||||
"network_flow_log_captured_sent",
|
||||
@@ -1419,7 +1433,9 @@
|
||||
"network_public_ip_shodan",
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_subnet_nsg_associated",
|
||||
"network_udp_internet_access_restricted",
|
||||
"network_vnet_ddos_protection_enabled",
|
||||
"network_watcher_enabled"
|
||||
]
|
||||
},
|
||||
@@ -1473,6 +1489,7 @@
|
||||
"network_public_ip_shodan",
|
||||
"network_rdp_internet_access_restricted",
|
||||
"network_ssh_internet_access_restricted",
|
||||
"network_subnet_nsg_associated",
|
||||
"network_udp_internet_access_restricted",
|
||||
"network_watcher_enabled"
|
||||
]
|
||||
@@ -1506,6 +1523,7 @@
|
||||
"Checks": [
|
||||
"app_minimum_tls_version_12",
|
||||
"cosmosdb_account_minimum_tls_version",
|
||||
"entra_app_registration_credential_not_expired",
|
||||
"monitor_storage_account_with_activity_logs_cmk_encrypted",
|
||||
"sqlserver_tde_encrypted_with_cmk",
|
||||
"sqlserver_tde_encryption_enabled",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"Description": "Adversaries may obtain and abuse credentials of existing accounts as a means of gaining Initial Access, Persistence, Privilege Escalation, or Defense Evasion. Compromised credentials may be used to bypass access controls placed on various resources on systems within the network and may even be used for persistent access to remote systems and externally available services, such as VPNs, Outlook Web Access, network devices, and remote desktop.[1] Compromised credentials may also grant an adversary increased privilege to specific systems or access to restricted areas of the network. Adversaries may choose not to use malware or tools in conjunction with the legitimate access those credentials provide to make it harder to detect their presence.",
|
||||
"TechniqueURL": "https://attack.mitre.org/techniques/T1078/",
|
||||
"Checks": [
|
||||
"entra_app_registration_credential_not_expired",
|
||||
"entra_conditional_access_policy_require_mfa_for_management_api",
|
||||
"entra_global_admin_in_less_than_five_users",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_authentication_methods_policy_strong_auth_enforced",
|
||||
"entra_non_privileged_user_has_mfa",
|
||||
"entra_privileged_user_has_mfa",
|
||||
"entra_security_defaults_enabled"
|
||||
@@ -493,7 +494,8 @@
|
||||
"keyvault_non_rbac_secret_expiration_set",
|
||||
"keyvault_logging_enabled",
|
||||
"keyvault_private_endpoints",
|
||||
"keyvault_access_only_through_private_endpoints"
|
||||
"keyvault_access_only_through_private_endpoints",
|
||||
"entra_app_registration_credential_not_expired"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -241,7 +241,8 @@
|
||||
"app_function_not_publicly_accessible",
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"network_public_ip_shodan",
|
||||
"storage_blob_public_access_level_is_disabled"
|
||||
"storage_blob_public_access_level_is_disabled",
|
||||
"entra_app_registration_credential_not_expired"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -307,7 +308,8 @@
|
||||
"app_minimum_tls_version_12",
|
||||
"mysql_flexible_server_minimum_tls_version_12",
|
||||
"sqlserver_recommended_minimal_tls_version",
|
||||
"storage_ensure_minimum_tls_version_12"
|
||||
"storage_ensure_minimum_tls_version_12",
|
||||
"network_subnet_nsg_associated"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1602,6 +1602,7 @@
|
||||
"cloudfront_distributions_origin_traffic_encrypted",
|
||||
"cloudfront_distributions_custom_ssl_certificate",
|
||||
"transfer_server_in_transit_encryption_enabled",
|
||||
"transfer_server_pqc_ssh_kex_enabled",
|
||||
"sagemaker_notebook_instance_encryption_enabled",
|
||||
"sagemaker_training_jobs_volume_and_output_encryption_enabled",
|
||||
"workspaces_volume_encryption_enabled",
|
||||
@@ -1785,6 +1786,7 @@
|
||||
"acm_certificates_with_secure_key_algorithms",
|
||||
"elb_insecure_ssl_ciphers",
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"transfer_server_pqc_ssh_kex_enabled",
|
||||
"cloudfront_distributions_using_deprecated_ssl_protocols"
|
||||
],
|
||||
"azure": [
|
||||
|
||||
@@ -1,597 +0,0 @@
|
||||
{
|
||||
"framework": "DORA",
|
||||
"name": "Digital Operational Resilience Act (Regulation (EU) 2022/2554)",
|
||||
"version": "2022/2554",
|
||||
"description": "The Digital Operational Resilience Act (DORA) is a European Union regulation (Regulation (EU) 2022/2554) that sets a uniform framework for the digital operational resilience of the EU financial sector. Mandatory since 17 January 2025, it applies to financial entities (banks, insurers, investment firms, payment institutions, etc.) and to ICT third-party service providers. DORA is structured around five pillars: ICT risk management, ICT-related incident reporting, digital operational resilience testing, ICT third-party risk management, and information sharing. This Prowler mapping covers the technical controls auditable from cloud configuration; the organisational, contractual and supervisory obligations defined in DORA must be addressed outside of Prowler.",
|
||||
"icon": "dora",
|
||||
"attributes_metadata": [
|
||||
{
|
||||
"key": "Pillar",
|
||||
"label": "Pillar",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing"
|
||||
],
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Article",
|
||||
"label": "Article",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "ArticleTitle",
|
||||
"label": "Article Title",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"table_config": {
|
||||
"group_by": "Pillar"
|
||||
},
|
||||
"pdf_config": {
|
||||
"language": "en",
|
||||
"primary_color": "#003399",
|
||||
"secondary_color": "#0055A5",
|
||||
"bg_color": "#F0F4FA",
|
||||
"group_by_field": "Pillar",
|
||||
"sections": [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing"
|
||||
],
|
||||
"section_short_names": {
|
||||
"ICT Risk Management": "ICT Risk Mgmt",
|
||||
"ICT-Related Incident Reporting": "Incident Reporting",
|
||||
"Digital Operational Resilience Testing": "Resilience Testing",
|
||||
"ICT Third-Party Risk Management": "Third-Party Risk",
|
||||
"Information Sharing": "Info Sharing"
|
||||
},
|
||||
"charts": [
|
||||
{
|
||||
"id": "pillar_compliance",
|
||||
"type": "horizontal_bar",
|
||||
"group_by": "Pillar",
|
||||
"title": "Compliance Score by DORA Pillar",
|
||||
"y_label": "Pillar",
|
||||
"x_label": "Compliance %",
|
||||
"value_source": "compliance_percent",
|
||||
"color_mode": "by_value"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"only_failed": true,
|
||||
"include_manual": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "DORA-Art5",
|
||||
"name": "Governance and organisation",
|
||||
"description": "Financial entities shall have a sound, comprehensive and well-documented ICT internal governance and control framework. Senior management is accountable for ICT risk and shall enforce strong identity, authentication and least-privilege policies for privileged identities, including the root account.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 5",
|
||||
"ArticleTitle": "Governance and organisation"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_avoid_root_usage",
|
||||
"iam_no_root_access_key",
|
||||
"iam_root_mfa_enabled",
|
||||
"iam_root_hardware_mfa_enabled",
|
||||
"iam_root_credentials_management_enabled",
|
||||
"iam_password_policy_minimum_length_14",
|
||||
"iam_password_policy_lowercase",
|
||||
"iam_password_policy_uppercase",
|
||||
"iam_password_policy_number",
|
||||
"iam_password_policy_symbol",
|
||||
"iam_password_policy_reuse_24",
|
||||
"iam_password_policy_expires_passwords_within_90_days_or_less",
|
||||
"iam_securityaudit_role_created",
|
||||
"iam_support_role_created",
|
||||
"organizations_account_part_of_organizations",
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_user_hardware_mfa_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art6",
|
||||
"name": "ICT risk management framework",
|
||||
"description": "Financial entities shall have an ICT risk management framework that is sound, comprehensive and well-documented, enabling them to address ICT risk quickly, efficiently and comprehensively and to ensure a high level of digital operational resilience. This includes continuous configuration recording, security findings aggregation and an enterprise-wide visibility plane.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 6",
|
||||
"ArticleTitle": "ICT risk management framework"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"config_recorder_all_regions_enabled",
|
||||
"config_recorder_using_aws_service_role",
|
||||
"securityhub_enabled",
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"organizations_delegated_administrators",
|
||||
"guardduty_centrally_managed",
|
||||
"guardduty_delegated_admin_enabled_all_regions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art7",
|
||||
"name": "ICT systems, protocols and tools",
|
||||
"description": "Financial entities shall use and maintain updated ICT systems, protocols and tools that are appropriate to the magnitude of operations supporting ICT functions, technologically resilient, and adequately equipped to securely process data. Cryptographic primitives, certificate hygiene and network segmentation are core to this requirement.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 7",
|
||||
"ArticleTitle": "ICT systems, protocols and tools"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"acm_certificates_with_secure_key_algorithms",
|
||||
"acm_certificates_transparency_logs_enabled",
|
||||
"acm_certificates_expiration_check",
|
||||
"ec2_ebs_default_encryption",
|
||||
"kms_cmk_rotation_enabled",
|
||||
"s3_bucket_secure_transport_policy",
|
||||
"s3_bucket_default_encryption",
|
||||
"s3_bucket_kms_encryption",
|
||||
"vpc_subnet_separate_private_public",
|
||||
"vpc_subnet_no_public_ip_by_default",
|
||||
"elb_insecure_ssl_ciphers",
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"elb_ssl_listeners",
|
||||
"elbv2_ssl_listeners",
|
||||
"cloudfront_distributions_using_deprecated_ssl_protocols",
|
||||
"cloudfront_distributions_https_enabled",
|
||||
"rds_instance_transport_encrypted"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art8",
|
||||
"name": "Identification",
|
||||
"description": "Financial entities shall identify, classify and adequately document all ICT supported business functions, roles and responsibilities, the information assets and ICT assets supporting them, and their interdependencies. They shall on a continuous basis identify all sources of ICT risk, in particular the risk exposure to and from other financial entities.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 8",
|
||||
"ArticleTitle": "Identification"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"macie_is_enabled",
|
||||
"macie_automated_sensitive_data_discovery_enabled",
|
||||
"ec2_securitygroup_not_used",
|
||||
"ec2_elastic_ip_unassigned",
|
||||
"ec2_networkacl_unused",
|
||||
"secretsmanager_secret_unused"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art9",
|
||||
"name": "Protection and prevention",
|
||||
"description": "Financial entities shall continuously monitor and control the security and functioning of ICT systems and tools and minimise the impact of ICT risk by deploying appropriate ICT security tools, policies and procedures. Encryption at rest and in transit, blocking of public exposure, network access controls, secret management and instance hardening are central to this article.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 9",
|
||||
"ArticleTitle": "Protection and prevention"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"kms_key_not_publicly_accessible",
|
||||
"ec2_ebs_volume_encryption",
|
||||
"ec2_ebs_snapshots_encrypted",
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_ebs_snapshot_account_block_public_access",
|
||||
"s3_account_level_public_access_blocks",
|
||||
"s3_bucket_level_public_access_block",
|
||||
"s3_bucket_public_access",
|
||||
"s3_bucket_policy_public_write_access",
|
||||
"s3_bucket_public_write_acl",
|
||||
"s3_bucket_public_list_acl",
|
||||
"s3_bucket_acl_prohibited",
|
||||
"s3_access_point_public_access_block",
|
||||
"ec2_securitygroup_default_restrict_traffic",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_cluster_storage_encrypted",
|
||||
"rds_instance_no_public_access",
|
||||
"rds_snapshots_public_access",
|
||||
"secretsmanager_not_publicly_accessible",
|
||||
"secretsmanager_has_restrictive_resource_policy",
|
||||
"secretsmanager_automatic_rotation_enabled",
|
||||
"dynamodb_tables_kms_cmk_encryption_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sns_topics_not_publicly_accessible",
|
||||
"ec2_instance_imdsv2_enabled",
|
||||
"ec2_instance_account_imdsv2_enabled",
|
||||
"efs_encryption_at_rest_enabled",
|
||||
"awslambda_function_not_publicly_accessible"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art10",
|
||||
"name": "Detection",
|
||||
"description": "Financial entities shall have in place mechanisms to promptly detect anomalous activities, including ICT network performance issues and ICT-related incidents, and to identify potential single points of failure. Threat detection across compute, identity, storage and the API control plane is required for timely detection.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 10",
|
||||
"ArticleTitle": "Detection"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"guardduty_ec2_malware_protection_enabled",
|
||||
"guardduty_lambda_protection_enabled",
|
||||
"guardduty_rds_protection_enabled",
|
||||
"guardduty_s3_protection_enabled",
|
||||
"guardduty_eks_audit_log_enabled",
|
||||
"guardduty_eks_runtime_monitoring_enabled",
|
||||
"securityhub_enabled",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"cloudtrail_insights_exist",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"ec2_elastic_ip_shodan"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art11",
|
||||
"name": "Response and recovery",
|
||||
"description": "Financial entities shall put in place a comprehensive ICT business continuity policy, including ICT response and recovery plans, that ensures the continuity of ICT-supported critical or important functions. Operational alarming, automated event routing and tested recovery actions are essential.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 11",
|
||||
"ArticleTitle": "Response and recovery"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudwatch_alarm_actions_enabled",
|
||||
"cloudwatch_alarm_actions_alarm_state_configured",
|
||||
"eventbridge_global_endpoint_event_replication_enabled",
|
||||
"sns_subscription_not_using_http_endpoints",
|
||||
"backup_plans_exist",
|
||||
"backup_vaults_exist",
|
||||
"rds_instance_critical_event_subscription",
|
||||
"rds_cluster_critical_event_subscription"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art12",
|
||||
"name": "Backup policies and procedures, restoration and recovery procedures and methods",
|
||||
"description": "Financial entities shall develop and document backup policies and procedures specifying the scope of data subject to backup and the minimum frequency of the backup, as well as restoration and recovery procedures and methods. Backups must be encrypted, retained, and resources must be designed for recoverability across availability zones and regions.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 12",
|
||||
"ArticleTitle": "Backup policies and procedures, restoration and recovery procedures and methods"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"backup_plans_exist",
|
||||
"backup_vaults_exist",
|
||||
"backup_vaults_encrypted",
|
||||
"backup_recovery_point_encrypted",
|
||||
"backup_reportplans_exist",
|
||||
"rds_instance_backup_enabled",
|
||||
"rds_cluster_protected_by_backup_plan",
|
||||
"rds_instance_protected_by_backup_plan",
|
||||
"rds_instance_multi_az",
|
||||
"rds_cluster_multi_az",
|
||||
"rds_cluster_backtrack_enabled",
|
||||
"rds_instance_deletion_protection",
|
||||
"rds_cluster_deletion_protection",
|
||||
"rds_snapshots_encrypted",
|
||||
"s3_bucket_object_versioning",
|
||||
"s3_bucket_object_lock",
|
||||
"s3_bucket_cross_region_replication",
|
||||
"s3_bucket_no_mfa_delete",
|
||||
"dynamodb_tables_pitr_enabled",
|
||||
"dynamodb_table_deletion_protection_enabled",
|
||||
"ec2_ebs_volume_protected_by_backup_plan",
|
||||
"ec2_ebs_volume_snapshots_exists",
|
||||
"autoscaling_group_multiple_az",
|
||||
"elb_is_in_multiple_az",
|
||||
"elbv2_is_in_multiple_az",
|
||||
"cloudfront_distributions_multiple_origin_failover_configured",
|
||||
"dynamodb_table_protected_by_backup_plan"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art13",
|
||||
"name": "Learning and evolving",
|
||||
"description": "Financial entities shall have in place capabilities and staff to gather information on vulnerabilities and cyber threats, perform post ICT-related incident reviews, and continuously feed lessons learnt back into the ICT risk assessment process. Findings aggregation and continuous insights drive this cycle.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 13",
|
||||
"ArticleTitle": "Learning and evolving"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"securityhub_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"inspector2_active_findings_exist",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_insights_exist"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art14",
|
||||
"name": "Communication",
|
||||
"description": "As part of the ICT risk management framework, financial entities shall have in place crisis communication plans enabling a responsible disclosure of ICT-related incidents or major vulnerabilities to clients, counterparts and the public. Reliable, encrypted and access-controlled notification channels are required.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 14",
|
||||
"ArticleTitle": "Communication"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sns_topics_not_publicly_accessible",
|
||||
"sns_subscription_not_using_http_endpoints",
|
||||
"eventbridge_bus_exposed",
|
||||
"eventbridge_bus_cross_account_access",
|
||||
"eventbridge_schema_registry_cross_account_access",
|
||||
"cloudwatch_alarm_actions_enabled",
|
||||
"cloudwatch_alarm_actions_alarm_state_configured"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art17",
|
||||
"name": "ICT-related incident management process",
|
||||
"description": "Financial entities shall define, establish and implement an ICT-related incident management process to detect, manage and notify ICT-related incidents. Comprehensive trail logging, log integrity protection, retention and centralisation of ICT events are foundational requirements.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 17",
|
||||
"ArticleTitle": "ICT-related incident management process"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_multi_region_enabled_logging_management_events",
|
||||
"cloudtrail_kms_encryption_enabled",
|
||||
"cloudtrail_log_file_validation_enabled",
|
||||
"cloudtrail_cloudwatch_logging_enabled",
|
||||
"cloudtrail_logs_s3_bucket_access_logging_enabled",
|
||||
"cloudtrail_logs_s3_bucket_is_not_publicly_accessible",
|
||||
"cloudtrail_s3_dataevents_read_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudtrail_bucket_requires_mfa_delete",
|
||||
"cloudtrail_bedrock_logging_enabled",
|
||||
"cloudwatch_log_group_retention_policy_specific_days_enabled",
|
||||
"cloudwatch_log_group_kms_encryption_enabled",
|
||||
"cloudwatch_log_group_no_secrets_in_logs",
|
||||
"cloudwatch_log_group_not_publicly_accessible",
|
||||
"vpc_flow_logs_enabled",
|
||||
"ec2_client_vpn_endpoint_connection_logging_enabled",
|
||||
"route53_public_hosted_zones_cloudwatch_logging_enabled",
|
||||
"elb_logging_enabled",
|
||||
"elbv2_logging_enabled",
|
||||
"cloudfront_distributions_logging_enabled",
|
||||
"s3_bucket_server_access_logging_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art18",
|
||||
"name": "Classification of ICT-related incidents and cyber threats",
|
||||
"description": "Financial entities shall classify ICT-related incidents and shall determine their impact based on criteria such as the number of clients affected, duration, geographical spread, data losses, and criticality of the services affected. Severity-aware threat detection across the estate underpins this classification.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 18",
|
||||
"ArticleTitle": "Classification of ICT-related incidents and cyber threats"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_no_high_severity_findings",
|
||||
"guardduty_centrally_managed",
|
||||
"guardduty_delegated_admin_enabled_all_regions",
|
||||
"securityhub_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art19",
|
||||
"name": "Reporting of major ICT-related incidents and voluntary notification of significant cyber threats",
|
||||
"description": "Financial entities shall report major ICT-related incidents to the relevant competent authority and may, on a voluntary basis, notify significant cyber threats. Detective metric filters, change-tracking alarms and reliable notification topics are needed to surface and route reportable events.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 19",
|
||||
"ArticleTitle": "Reporting of major ICT-related incidents and voluntary notification of significant cyber threats"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudwatch_log_metric_filter_authentication_failures",
|
||||
"cloudwatch_log_metric_filter_unauthorized_api_calls",
|
||||
"cloudwatch_log_metric_filter_root_usage",
|
||||
"cloudwatch_log_metric_filter_sign_in_without_mfa",
|
||||
"cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk",
|
||||
"cloudwatch_log_metric_filter_for_s3_bucket_policy_changes",
|
||||
"cloudwatch_log_metric_filter_policy_changes",
|
||||
"cloudwatch_log_metric_filter_security_group_changes",
|
||||
"cloudwatch_log_metric_filter_aws_organizations_changes",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
|
||||
"cloudwatch_changes_to_network_acls_alarm_configured",
|
||||
"cloudwatch_changes_to_network_gateways_alarm_configured",
|
||||
"cloudwatch_changes_to_network_route_tables_alarm_configured",
|
||||
"cloudwatch_changes_to_vpcs_alarm_configured",
|
||||
"sns_subscription_not_using_http_endpoints"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art24",
|
||||
"name": "General requirements for the performance of digital operational resilience testing",
|
||||
"description": "Financial entities shall establish, maintain and review a sound and comprehensive digital operational resilience testing programme, as an integral part of the ICT risk management framework. Continuous vulnerability discovery, configuration assessment and instance manageability are foundational.",
|
||||
"attributes": {
|
||||
"Pillar": "Digital Operational Resilience Testing",
|
||||
"Article": "Article 24",
|
||||
"ArticleTitle": "General requirements for the performance of digital operational resilience testing"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"securityhub_enabled",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"ec2_instance_with_outdated_ami",
|
||||
"ssm_managed_compliant_patching"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art25",
|
||||
"name": "Testing of ICT tools and systems",
|
||||
"description": "Financial entities shall ensure that tests are undertaken on ICT tools and systems, on critical ICT systems supporting all critical or important functions, at least yearly. Vulnerability assessments, deprecated component detection and certificate hygiene must be tracked.",
|
||||
"attributes": {
|
||||
"Pillar": "Digital Operational Resilience Testing",
|
||||
"Article": "Article 25",
|
||||
"ArticleTitle": "Testing of ICT tools and systems"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"config_recorder_all_regions_enabled",
|
||||
"ec2_instance_with_outdated_ami",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"ec2_instance_paravirtual_type",
|
||||
"rds_instance_deprecated_engine_version",
|
||||
"acm_certificates_expiration_check",
|
||||
"rds_instance_certificate_expiration",
|
||||
"iam_no_expired_server_certificates_stored",
|
||||
"ssm_managed_compliant_patching"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art28",
|
||||
"name": "General principles (ICT third-party risk)",
|
||||
"description": "Financial entities shall manage ICT third-party risk as an integral component of ICT risk within their ICT risk management framework. Cross-account access, trust boundaries, organization-level controls and dependency visibility are critical to monitor third-party exposure on AWS.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Third-Party Risk Management",
|
||||
"Article": "Article 28",
|
||||
"ArticleTitle": "General principles (ICT third-party risk)"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_role_cross_service_confused_deputy_prevention",
|
||||
"iam_role_cross_account_readonlyaccess_policy",
|
||||
"iam_no_custom_policy_permissive_role_assumption",
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"s3_bucket_cross_account_access",
|
||||
"dynamodb_table_cross_account_access",
|
||||
"eventbridge_bus_cross_account_access",
|
||||
"eventbridge_schema_registry_cross_account_access",
|
||||
"cloudwatch_cross_account_sharing_disabled",
|
||||
"organizations_delegated_administrators",
|
||||
"organizations_account_part_of_organizations",
|
||||
"organizations_scp_check_deny_regions",
|
||||
"vpc_endpoint_connections_trust_boundaries",
|
||||
"vpc_endpoint_services_allowed_principals_trust_boundaries",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
"awslambda_function_using_cross_account_layers"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art30",
|
||||
"name": "Key contractual provisions",
|
||||
"description": "Contractual arrangements with ICT third-party service providers shall be set out in writing and include, at minimum, agreed service levels and clear allocation of rights and obligations. Privilege boundaries, least-privilege policies and absence of administrative wildcards are the technical guardrails that enforce these contractual constraints inside AWS.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Third-Party Risk Management",
|
||||
"Article": "Article 30",
|
||||
"ArticleTitle": "Key contractual provisions"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_unattached_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_allows_privilege_escalation",
|
||||
"iam_policy_allows_privilege_escalation",
|
||||
"iam_inline_policy_no_full_access_to_cloudtrail",
|
||||
"iam_inline_policy_no_full_access_to_kms",
|
||||
"iam_policy_no_full_access_to_cloudtrail",
|
||||
"iam_policy_no_full_access_to_kms",
|
||||
"iam_role_administratoraccess_policy",
|
||||
"iam_user_administrator_access_policy",
|
||||
"iam_group_administrator_access_policy",
|
||||
"iam_administrator_access_with_mfa",
|
||||
"iam_policy_attached_only_to_group_or_roles",
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art45",
|
||||
"name": "Information-sharing arrangements on cyber threat information and intelligence",
|
||||
"description": "Financial entities may exchange amongst themselves cyber threat information and intelligence, including indicators of compromise, tactics, techniques and procedures, cyber security alerts and configuration tools. Centralised threat detection, sensitive data discovery and trail-based intelligence enable participation in such information-sharing arrangements.",
|
||||
"attributes": {
|
||||
"Pillar": "Information Sharing",
|
||||
"Article": "Article 45",
|
||||
"ArticleTitle": "Information-sharing arrangements on cyber threat information and intelligence"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_centrally_managed",
|
||||
"securityhub_enabled",
|
||||
"macie_is_enabled",
|
||||
"macie_automated_sensitive_data_discovery_enabled",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"accessanalyzer_enabled_without_findings"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.31.0"
|
||||
prowler_version = "5.32.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
@@ -80,6 +80,7 @@ class Provider(str, Enum):
|
||||
VERCEL = "vercel"
|
||||
OKTA = "okta"
|
||||
STACKIT = "stackit"
|
||||
LINODE = "linode"
|
||||
|
||||
|
||||
# Compliance
|
||||
@@ -288,6 +289,11 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
Returns:
|
||||
dict: The configuration dictionary for the specified provider.
|
||||
"""
|
||||
# Imported lazily to avoid an import cycle: schemas may eventually want to
|
||||
# import from prowler.config.config (e.g. for shared constants).
|
||||
from prowler.config.schema.registry import SCHEMAS
|
||||
from prowler.config.schema.validator import validate_provider_config
|
||||
|
||||
try:
|
||||
with open(config_file_path, "r", encoding=encoding_format_utf_8) as f:
|
||||
config_file = yaml.safe_load(f)
|
||||
@@ -313,7 +319,11 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
else:
|
||||
config = {}
|
||||
|
||||
return config
|
||||
return validate_provider_config(
|
||||
provider=provider,
|
||||
raw=config,
|
||||
schema_cls=SCHEMAS.get(provider),
|
||||
)
|
||||
|
||||
except FileNotFoundError as error:
|
||||
logger.error(
|
||||
|
||||
@@ -380,6 +380,39 @@ aws:
|
||||
# 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
|
||||
# Allowed post-quantum key algorithms for AWS Private CA certificate authorities
|
||||
acmpca_pqc_key_algorithms:
|
||||
- "ML_DSA_44"
|
||||
- "ML_DSA_65"
|
||||
- "ML_DSA_87"
|
||||
# aws.cloudfront_distributions_pqc_tls_enabled
|
||||
# Allowed CloudFront MinimumProtocolVersion values that enable post-quantum hybrid key exchange
|
||||
cloudfront_pqc_min_protocol_versions:
|
||||
- "TLSv1.3_2025"
|
||||
# aws.apigateway_domain_name_pqc_tls_enabled
|
||||
# Allowed post-quantum TLS security policies for API Gateway custom domain names
|
||||
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.rolesanywhere_trust_anchor_pqc_pki
|
||||
# Allowed post-quantum key algorithms for AWS Private CAs backing IAM Roles Anywhere trust anchors
|
||||
rolesanywhere_pqc_pca_key_algorithms:
|
||||
- "ML_DSA_44"
|
||||
- "ML_DSA_65"
|
||||
- "ML_DSA_87"
|
||||
|
||||
# AWS Post-Quantum SSH Key Exchange Configuration
|
||||
# aws.transfer_server_pqc_ssh_kex_enabled
|
||||
# Allowed AWS Transfer Family security policies with post-quantum SSH key exchange
|
||||
transfer_pqc_ssh_allowed_policies:
|
||||
- "TransferSecurityPolicy-2025-03"
|
||||
- "TransferSecurityPolicy-FIPS-2025-03"
|
||||
- "TransferSecurityPolicy-AS2Restricted-2025-07"
|
||||
|
||||
# AWS Elasticache Configuration
|
||||
# aws.elasticache_redis_cluster_backup_enabled
|
||||
# Minimum number of days that a Redis cluster must have backups retention period
|
||||
@@ -549,6 +582,9 @@ gcp:
|
||||
# GCP Storage Sufficient Retention Period
|
||||
# gcp.cloudstorage_bucket_sufficient_retention_period
|
||||
storage_min_retention_days: 90
|
||||
# GCP Secret Manager Rotation Period
|
||||
# gcp.secretmanager_secret_rotation_enabled
|
||||
secretmanager_max_rotation_days: 90
|
||||
|
||||
# Kubernetes Configuration
|
||||
kubernetes:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
### Account, Check and/or Region can be * to apply for all the cases.
|
||||
### Account == <Linode Account UUID>
|
||||
### Region == * (Linode is non-regional)
|
||||
### Resources and tags are lists that can have either Regex or Keywords.
|
||||
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
|
||||
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
|
||||
### For each check you can except Accounts, Regions, Resources and/or Tags.
|
||||
########################### MUTELIST EXAMPLE ###########################
|
||||
Mutelist:
|
||||
Accounts:
|
||||
"example-account-uuid":
|
||||
Checks:
|
||||
"administration_user_2fa_enabled":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "example-user@example.com"
|
||||
- "another-user@example.com"
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Bridge between the Pydantic-based provider schemas in
|
||||
`prowler.config.schema` and the Prowler App backend (Django) + UI.
|
||||
|
||||
The SDK runtime is intentionally LENIENT: invalid keys are dropped with a
|
||||
warning and downstream checks fall back to their defaults
|
||||
(`prowler.config.schema.validator.validate_provider_config`).
|
||||
|
||||
The Prowler App, however, needs to surface those errors to the user when
|
||||
they save a Scan Config from the UI, and to expose the schema as JSON so
|
||||
the UI can validate live with `ajv`. This module provides:
|
||||
|
||||
- `validate_scan_config(payload)` — STRICT: returns a list of
|
||||
`{path, message}` errors without silently dropping anything. The DRF
|
||||
serializer (`api/.../v1/serializers.py:validate_scan_config_payload`)
|
||||
turns each entry into a `ValidationError`.
|
||||
|
||||
- `SCAN_CONFIG_SCHEMA` — aggregated JSON Schema derived from the Pydantic
|
||||
models via `model_json_schema()`. Served by the `/scan-configs/schema`
|
||||
endpoint and consumed by the UI editor for in-editor live validation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from prowler.config.schema.registry import SCHEMAS
|
||||
|
||||
|
||||
def _format_loc(loc: tuple) -> str:
|
||||
"""Render a Pydantic error location as a dot-separated path.
|
||||
|
||||
Integer elements (array indices) are formatted as `[idx]` appended to the
|
||||
previous component. String elements are joined with dots. An empty location
|
||||
is rendered as `<root>`.
|
||||
|
||||
Examples:
|
||||
("aws", "regions", 0) -> "aws.regions[0]"
|
||||
("aws", "threshold") -> "aws.threshold"
|
||||
() -> "<root>"
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for piece in loc:
|
||||
if isinstance(piece, int):
|
||||
if parts:
|
||||
parts[-1] = f"{parts[-1]}[{piece}]"
|
||||
else:
|
||||
parts.append(f"[{piece}]")
|
||||
else:
|
||||
parts.append(str(piece))
|
||||
return ".".join(parts) if parts else "<root>"
|
||||
|
||||
|
||||
def validate_scan_config(payload: Any) -> list[dict]:
|
||||
"""Validate a scan config payload against the registered provider schemas.
|
||||
|
||||
Strict by design: every Pydantic violation surfaces as a `{path, message}`
|
||||
entry so the caller can decide how to present it. Unknown provider
|
||||
sections are accepted (consistent with `additionalProperties: True` at
|
||||
the top level — the SDK simply has no opinion on them).
|
||||
"""
|
||||
if not isinstance(payload, dict):
|
||||
return [
|
||||
{
|
||||
"path": "<root>",
|
||||
"message": "Scan config must be a mapping with provider sections.",
|
||||
}
|
||||
]
|
||||
|
||||
errors: list[dict] = []
|
||||
for provider, section in payload.items():
|
||||
schema_cls = SCHEMAS.get(provider)
|
||||
if schema_cls is None:
|
||||
# Unknown provider type: tolerated. The SDK will simply ignore it.
|
||||
continue
|
||||
if not isinstance(section, dict):
|
||||
errors.append(
|
||||
{
|
||||
"path": str(provider),
|
||||
"message": "section must be a mapping.",
|
||||
}
|
||||
)
|
||||
continue
|
||||
try:
|
||||
schema_cls.model_validate(section)
|
||||
except ValidationError as exc:
|
||||
for err in exc.errors():
|
||||
loc = err.get("loc") or ()
|
||||
path = _format_loc((str(provider), *loc))
|
||||
errors.append(
|
||||
{
|
||||
"path": path,
|
||||
"message": err.get("msg", "validation error"),
|
||||
}
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _build_aggregated_schema() -> dict:
|
||||
"""Compose one JSON Schema per provider into a single top-level schema.
|
||||
|
||||
The output mirrors the layout of `prowler/config/config.yaml` (a mapping
|
||||
keyed by provider type) and is what the UI consumes via `ajv`.
|
||||
"""
|
||||
properties: dict[str, dict] = {}
|
||||
for provider, schema_cls in SCHEMAS.items():
|
||||
properties[provider] = schema_cls.model_json_schema()
|
||||
return {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Prowler Scan Config",
|
||||
"type": "object",
|
||||
"additionalProperties": True,
|
||||
"properties": properties,
|
||||
}
|
||||
|
||||
|
||||
SCAN_CONFIG_SCHEMA: dict = _build_aggregated_schema()
|
||||
@@ -0,0 +1,422 @@
|
||||
"""AWS provider config schema.
|
||||
|
||||
Bounds on every field are intentionally conservative: they are not the
|
||||
absolute service maxima but the values that produce a useful security
|
||||
check. A user is free to keep the built-in default by omitting the key —
|
||||
out-of-range values are dropped with a warning at SDK runtime, and
|
||||
rejected at the Prowler App backend.
|
||||
|
||||
Whenever an upper bound is uncertain, the cap is set to a value that
|
||||
still keeps the check meaningful (e.g. a 10-year window for date-based
|
||||
thresholds) and avoids ints that obviously break downstream maths
|
||||
(`min_kinesis_stream_retention_hours = 99999`).
|
||||
"""
|
||||
|
||||
from typing import Annotated, Literal, Optional
|
||||
|
||||
from pydantic import AfterValidator, Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
from prowler.config.schema.validators import (
|
||||
make_dotted_version_validator,
|
||||
validate_ip_networks,
|
||||
validate_port_range,
|
||||
)
|
||||
|
||||
# ---- Reusable constants -----------------------------------------------------
|
||||
|
||||
# CloudWatch Logs only accepts these retention values (in days). Anything else
|
||||
# is silently coerced to the next valid value by the API — we reject upfront.
|
||||
_CLOUDWATCH_RETENTION_DAYS = (
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
14,
|
||||
30,
|
||||
60,
|
||||
90,
|
||||
120,
|
||||
150,
|
||||
180,
|
||||
365,
|
||||
400,
|
||||
545,
|
||||
731,
|
||||
1096,
|
||||
1827,
|
||||
2192,
|
||||
2557,
|
||||
2922,
|
||||
3288,
|
||||
3653,
|
||||
)
|
||||
|
||||
_VALID_CW_RETENTION_LITERAL = Literal[
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
14,
|
||||
30,
|
||||
60,
|
||||
90,
|
||||
120,
|
||||
150,
|
||||
180,
|
||||
365,
|
||||
400,
|
||||
545,
|
||||
731,
|
||||
1096,
|
||||
1827,
|
||||
2192,
|
||||
2557,
|
||||
2922,
|
||||
3288,
|
||||
3653,
|
||||
]
|
||||
|
||||
|
||||
# ---- Custom validators ------------------------------------------------------
|
||||
|
||||
|
||||
# Reusable validators shared across providers (see schema/validators.py).
|
||||
_validate_port_range = validate_port_range
|
||||
_validate_trusted_ips = validate_ip_networks
|
||||
# "1.4.0" style strings (used by Fargate platform versions).
|
||||
_validate_semver = make_dotted_version_validator(3, 3)
|
||||
# "1.28" style strings (EKS minor versions).
|
||||
_validate_eks_minor = make_dotted_version_validator(2, 2)
|
||||
|
||||
|
||||
def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]:
|
||||
if v is None:
|
||||
return v
|
||||
for account_id in v:
|
||||
if not (account_id.isdigit() and len(account_id) == 12):
|
||||
raise ValueError(
|
||||
f"trusted_account_ids entry {account_id!r} is not a 12-digit AWS account id"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
# ---- Nested models ----------------------------------------------------------
|
||||
|
||||
|
||||
class _DetectSecretsPlugin(ProviderConfigBase):
|
||||
"""One entry inside ``detect_secrets_plugins``.
|
||||
|
||||
Only ``name`` is required by the upstream library. ``limit`` is used by
|
||||
the entropy detectors. Any other plugin-specific kwarg is preserved by
|
||||
the ``extra="allow"`` policy inherited from ProviderConfigBase.
|
||||
"""
|
||||
|
||||
name: str
|
||||
limit: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=10.0,
|
||||
description=(
|
||||
"Entropy threshold for detect-secrets entropy plugins. Range: 0..10 "
|
||||
"(Shannon entropy is bounded by log2(256)=8; >10 is meaningless)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---- Main schema ------------------------------------------------------------
|
||||
|
||||
|
||||
class AWSProviderConfig(ProviderConfigBase):
|
||||
# --- IAM ---------------------------------------------------------------
|
||||
mute_non_default_regions: Optional[bool] = None
|
||||
disallowed_regions: Optional[list[str]] = None
|
||||
max_unused_access_keys_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=180,
|
||||
description=(
|
||||
"Days an IAM user access key can stay unused before being flagged. "
|
||||
"Range: 30..180 days (CIS AWS 1.13 recommends 45; NIST IA-5 ≤90)."
|
||||
),
|
||||
)
|
||||
max_console_access_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=180,
|
||||
description=(
|
||||
"Days an IAM console password can stay unused before being flagged. "
|
||||
"Range: 30..180 days (CIS AWS 1.12 recommends 45)."
|
||||
),
|
||||
)
|
||||
max_unused_sagemaker_access_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=180,
|
||||
description=(
|
||||
"Days a SageMaker user access key can stay unused. Range: 7..180 "
|
||||
"(SageMaker tokens are usually high-privilege over S3/KMS)."
|
||||
),
|
||||
)
|
||||
|
||||
# --- EC2 ---------------------------------------------------------------
|
||||
shodan_api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=512,
|
||||
description="API key for Shodan lookups on EC2 public IPs.",
|
||||
)
|
||||
max_security_group_rules: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=1000,
|
||||
description="Max ingress+egress rules per security group. AWS hard limit is 1000.",
|
||||
)
|
||||
max_ec2_instance_age_in_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=1095,
|
||||
description=(
|
||||
"Days an EC2 instance can run before being flagged as old. "
|
||||
"Range: 1..1095 (3 years; instances should be refreshed for patching "
|
||||
"per NIST CM-3 — anything older is a security smell)."
|
||||
),
|
||||
)
|
||||
ec2_allowed_interface_types: Optional[list[str]] = None
|
||||
ec2_allowed_instance_owners: Optional[list[str]] = None
|
||||
ec2_high_risk_ports: Annotated[
|
||||
Optional[list[int]], AfterValidator(_validate_port_range)
|
||||
] = Field(
|
||||
default=None,
|
||||
description="TCP/UDP ports considered high-risk when reachable from the Internet (1..65535; port 0 is reserved).",
|
||||
)
|
||||
|
||||
# --- ECS ---------------------------------------------------------------
|
||||
fargate_linux_latest_version: Annotated[
|
||||
Optional[str], AfterValidator(_validate_semver)
|
||||
] = Field(default=None, description="Fargate Linux platform version (X.Y.Z).")
|
||||
fargate_windows_latest_version: Annotated[
|
||||
Optional[str], AfterValidator(_validate_semver)
|
||||
] = Field(default=None, description="Fargate Windows platform version (X.Y.Z).")
|
||||
|
||||
# --- Cross-account trust ----------------------------------------------
|
||||
trusted_account_ids: Annotated[
|
||||
Optional[list[str]], AfterValidator(_validate_account_ids)
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Additional 12-digit AWS account IDs trusted by cross-account checks.",
|
||||
)
|
||||
trusted_ips: Annotated[
|
||||
Optional[list[str]], AfterValidator(_validate_trusted_ips)
|
||||
] = Field(
|
||||
default=None,
|
||||
description="IPv4/IPv6 addresses or CIDR ranges that are NOT considered public.",
|
||||
)
|
||||
|
||||
# --- CloudWatch / CloudFormation --------------------------------------
|
||||
log_group_retention_days: Optional[_VALID_CW_RETENTION_LITERAL] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Required CloudWatch Logs retention in days. Must match one of the "
|
||||
f"values accepted by the AWS API: {list(_CLOUDWATCH_RETENTION_DAYS)}."
|
||||
),
|
||||
)
|
||||
recommended_cdk_bootstrap_version: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Min CDK bootstrap version expected on the account.",
|
||||
)
|
||||
|
||||
# --- AppStream --------------------------------------------------------
|
||||
max_idle_disconnect_timeout_in_seconds: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=60,
|
||||
le=1800,
|
||||
description=(
|
||||
"AppStream idle disconnect timeout (seconds). Range: 60..1800 "
|
||||
"(NIST AC-12: sensitive sessions ≤15 min — cap at 30 min)."
|
||||
),
|
||||
)
|
||||
max_disconnect_timeout_in_seconds: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=60,
|
||||
le=3600,
|
||||
description="AppStream disconnect timeout (seconds). Range: 60..3600.",
|
||||
)
|
||||
max_session_duration_seconds: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=600,
|
||||
le=86400,
|
||||
description=(
|
||||
"AppStream max session duration (seconds). Range: 600..86400 "
|
||||
"(10 min .. 24 h — AWS AppStream hard limit per session)."
|
||||
),
|
||||
)
|
||||
|
||||
# --- Lambda -----------------------------------------------------------
|
||||
obsolete_lambda_runtimes: Optional[list[str]] = None
|
||||
lambda_min_azs: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=6,
|
||||
description="Min number of AZs a VPC-bound Lambda must span. Range: 1..6.",
|
||||
)
|
||||
|
||||
# --- Organizations ----------------------------------------------------
|
||||
organizations_enabled_regions: Optional[list[str]] = None
|
||||
organizations_trusted_delegated_administrators: Annotated[
|
||||
Optional[list[str]], AfterValidator(_validate_account_ids)
|
||||
] = None
|
||||
organizations_trusted_ids: Optional[list[str]] = None
|
||||
|
||||
# --- ECR --------------------------------------------------------------
|
||||
ecr_repository_vulnerability_minimum_severity: Optional[
|
||||
Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Highest severity tolerated for ECR images.",
|
||||
)
|
||||
|
||||
# --- Trusted Advisor --------------------------------------------------
|
||||
verify_premium_support_plans: Optional[bool] = None
|
||||
|
||||
# --- CloudTrail threat detection: privilege escalation ----------------
|
||||
threat_detection_privilege_escalation_threshold: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Fraction of suspicious actions that triggers the priv-esc detection.",
|
||||
)
|
||||
threat_detection_privilege_escalation_minutes: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=5,
|
||||
le=43200,
|
||||
description=(
|
||||
"Lookback window (minutes) for priv-esc detection. Range: 5..43200 "
|
||||
"(under 5 min the signal is dominated by false positives)."
|
||||
),
|
||||
)
|
||||
threat_detection_privilege_escalation_actions: Optional[list[str]] = None
|
||||
|
||||
# --- CloudTrail threat detection: enumeration -------------------------
|
||||
threat_detection_enumeration_threshold: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Fraction of suspicious actions that triggers the enumeration detection.",
|
||||
)
|
||||
threat_detection_enumeration_minutes: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=5,
|
||||
le=43200,
|
||||
description="Lookback window (minutes) for enumeration detection. Range: 5..43200.",
|
||||
)
|
||||
threat_detection_enumeration_actions: Optional[list[str]] = None
|
||||
|
||||
# --- CloudTrail threat detection: LLM jacking -------------------------
|
||||
threat_detection_llm_jacking_threshold: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Fraction of suspicious actions that triggers the LLM-jacking detection.",
|
||||
)
|
||||
threat_detection_llm_jacking_minutes: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=5,
|
||||
le=43200,
|
||||
description="Lookback window (minutes) for LLM-jacking detection. Range: 5..43200.",
|
||||
)
|
||||
threat_detection_llm_jacking_actions: Optional[list[str]] = None
|
||||
|
||||
# --- RDS --------------------------------------------------------------
|
||||
check_rds_instance_replicas: Optional[bool] = None
|
||||
|
||||
# --- ACM --------------------------------------------------------------
|
||||
days_to_expire_threshold: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=365,
|
||||
description=(
|
||||
"Days before certificate expiration to flag. Range: 7..365 "
|
||||
"(PCI-DSS 4.2.1.1: alert ≥30 days before expiry; <7 days is too "
|
||||
"tight to actually act on)."
|
||||
),
|
||||
)
|
||||
insecure_key_algorithms: Optional[list[str]] = None
|
||||
|
||||
# --- EKS --------------------------------------------------------------
|
||||
eks_required_log_types: Optional[
|
||||
list[
|
||||
Literal[
|
||||
"api",
|
||||
"audit",
|
||||
"authenticator",
|
||||
"controllerManager",
|
||||
"scheduler",
|
||||
]
|
||||
]
|
||||
] = Field(
|
||||
default=None,
|
||||
description="EKS control plane log types that must be enabled.",
|
||||
)
|
||||
eks_cluster_oldest_version_supported: Annotated[
|
||||
Optional[str], AfterValidator(_validate_eks_minor)
|
||||
] = Field(
|
||||
default=None,
|
||||
description='Minimum supported EKS minor version, expected as "X.Y".',
|
||||
)
|
||||
|
||||
# --- CodeBuild --------------------------------------------------------
|
||||
excluded_sensitive_environment_variables: Optional[list[str]] = None
|
||||
codebuild_github_allowed_organizations: Optional[list[str]] = None
|
||||
|
||||
# --- ELB / ELBv2 ------------------------------------------------------
|
||||
elb_min_azs: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=6,
|
||||
description="Min AZs a Classic ELB must span. Range: 1..6.",
|
||||
)
|
||||
elbv2_min_azs: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=6,
|
||||
description="Min AZs an Application/Network LB must span. Range: 1..6.",
|
||||
)
|
||||
|
||||
# --- ElastiCache -----------------------------------------------------
|
||||
minimum_snapshot_retention_period: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=35,
|
||||
description="Days an ElastiCache backup must be retained. Range: 1..35 (service hard limit).",
|
||||
)
|
||||
|
||||
# --- Secrets ---------------------------------------------------------
|
||||
secrets_ignore_patterns: Optional[list[str]] = None
|
||||
max_days_secret_unused: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=365,
|
||||
description="Days a Secrets Manager secret can stay unused. Range: 7..365.",
|
||||
)
|
||||
max_days_secret_unrotated: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=180,
|
||||
description=(
|
||||
"Days a Secrets Manager secret can go without rotation. Range: 1..180 "
|
||||
"(NIST IA-5: rotate quarterly; CIS recommends ≤90)."
|
||||
),
|
||||
)
|
||||
|
||||
# --- Kinesis ---------------------------------------------------------
|
||||
min_kinesis_stream_retention_hours: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=24,
|
||||
le=8760,
|
||||
description="Hours of Kinesis stream retention. Range: 24..8760 (1 day .. 1 year).",
|
||||
)
|
||||
|
||||
# --- detect-secrets plugin list -------------------------------------
|
||||
detect_secrets_plugins: Optional[list[_DetectSecretsPlugin]] = None
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Azure provider config schema with safety bounds.
|
||||
|
||||
Bounds aim for values that produce a meaningful security check; out-of-range
|
||||
values are dropped (SDK runtime) or rejected (Prowler App backend).
|
||||
"""
|
||||
|
||||
from typing import Annotated, Literal, Optional
|
||||
|
||||
from pydantic import AfterValidator, Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
from prowler.config.schema.validators import make_dotted_version_validator
|
||||
|
||||
# Accept "8.2", "3.12", "17" style version strings. Used by App Service
|
||||
# language version fields where the upstream APIs accept either MAJOR or
|
||||
# MAJOR.MINOR notation.
|
||||
_validate_dotted_version = make_dotted_version_validator(1, 2)
|
||||
|
||||
|
||||
class AzureProviderConfig(ProviderConfigBase):
|
||||
# --- Network ---------------------------------------------------------
|
||||
shodan_api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=512,
|
||||
description="API key for Shodan lookups on Azure public IPs.",
|
||||
)
|
||||
|
||||
# --- Defender --------------------------------------------------------
|
||||
defender_attack_path_minimal_risk_level: Optional[
|
||||
Literal["Low", "Medium", "High", "Critical"]
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Minimum attack-path risk level worth a notification.",
|
||||
)
|
||||
|
||||
# --- App Service ----------------------------------------------------
|
||||
php_latest_version: Annotated[
|
||||
Optional[str], AfterValidator(_validate_dotted_version)
|
||||
] = Field(default=None, description='PHP minimum acceptable version, e.g. "8.2".')
|
||||
python_latest_version: Annotated[
|
||||
Optional[str], AfterValidator(_validate_dotted_version)
|
||||
] = Field(
|
||||
default=None, description='Python minimum acceptable version, e.g. "3.12".'
|
||||
)
|
||||
java_latest_version: Annotated[
|
||||
Optional[str], AfterValidator(_validate_dotted_version)
|
||||
] = Field(default=None, description='Java minimum acceptable version, e.g. "17".')
|
||||
|
||||
# --- SQL ------------------------------------------------------------
|
||||
recommended_minimal_tls_versions: Optional[list[Literal["1.2", "1.3"]]] = Field(
|
||||
default=None,
|
||||
description="TLS versions accepted on Azure SQL Server.",
|
||||
)
|
||||
|
||||
# --- Virtual Machines -----------------------------------------------
|
||||
desired_vm_sku_sizes: Optional[list[str]] = None
|
||||
vm_backup_min_daily_retention_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=9999,
|
||||
description=(
|
||||
"Min daily backup retention days. Range: 7..9999 "
|
||||
"(Azure Backup hard limit; <7 days defeats DR/ransomware recovery)."
|
||||
),
|
||||
)
|
||||
|
||||
# --- API Management threat detection (LLM jacking) -----------------
|
||||
apim_threat_detection_llm_jacking_threshold: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Fraction of suspicious actions that triggers the detection.",
|
||||
)
|
||||
apim_threat_detection_llm_jacking_minutes: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=5,
|
||||
le=43200,
|
||||
description=(
|
||||
"Lookback window (minutes) for LLM-jacking detection. Range: 5..43200 "
|
||||
"(under 5 min the signal is dominated by false positives)."
|
||||
),
|
||||
)
|
||||
apim_threat_detection_llm_jacking_actions: Optional[list[str]] = None
|
||||
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class ProviderConfigBase(BaseModel):
|
||||
"""Base for every provider config schema.
|
||||
|
||||
``extra="allow"`` is REQUIRED for backwards compatibility: third-party
|
||||
check plugins frequently introduce config keys we do not know about,
|
||||
and pre-existing user configs may carry deprecated keys. Validation
|
||||
must never reject these.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="allow",
|
||||
str_strip_whitespace=True,
|
||||
validate_assignment=False,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Cloudflare provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class CloudflareProviderConfig(ProviderConfigBase):
|
||||
"""Cloudflare provider configuration schema.
|
||||
|
||||
Defines optional configuration parameters for Cloudflare security checks,
|
||||
including API retry behavior.
|
||||
"""
|
||||
|
||||
max_retries: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=0,
|
||||
le=10,
|
||||
description=(
|
||||
"Max retries for Cloudflare API requests. Range: 0..10 (0 disables retries)."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""GCP provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class GCPProviderConfig(ProviderConfigBase):
|
||||
shodan_api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=512,
|
||||
description="API key for Shodan lookups on GCP public IPs.",
|
||||
)
|
||||
mig_min_zones: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=5,
|
||||
description="Min zones a Managed Instance Group must span. Range: 1..5.",
|
||||
)
|
||||
max_snapshot_age_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=1095,
|
||||
description=(
|
||||
"Days a disk snapshot can age before being flagged. Range: 1..1095 "
|
||||
"(3 years; older snapshots typically miss data-class compliance)."
|
||||
),
|
||||
)
|
||||
max_unused_account_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=365,
|
||||
description=(
|
||||
"Days a service account or user-managed key can stay unused. "
|
||||
"Range: 30..365."
|
||||
),
|
||||
)
|
||||
storage_min_retention_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=3650,
|
||||
description="Min retention period on Cloud Storage buckets. Range: 1..3650.",
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""GitHub provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class GitHubProviderConfig(ProviderConfigBase):
|
||||
inactive_not_archived_days_threshold: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=3650,
|
||||
description=(
|
||||
"Days a repository can stay inactive without being archived before "
|
||||
"being flagged. Range: 30..3650 (CIS GitHub recommends 180; "
|
||||
"<30 days produces false positives on seasonal projects)."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Kubernetes provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class KubernetesProviderConfig(ProviderConfigBase):
|
||||
audit_log_maxbackup: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=2,
|
||||
le=1000,
|
||||
description=(
|
||||
"API server audit log file rotations to keep. Range: 2..1000 "
|
||||
"(CIS Kubernetes 1.2.18 recommends ≥10)."
|
||||
),
|
||||
)
|
||||
audit_log_maxsize: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=10,
|
||||
le=10000,
|
||||
description=(
|
||||
"Max MB per audit log file before rotation. Range: 10..10000 MB "
|
||||
"(CIS Kubernetes 1.2.19 recommends ≥100 MB)."
|
||||
),
|
||||
)
|
||||
audit_log_maxage: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=3650,
|
||||
description=(
|
||||
"Days an audit log file is retained. Range: 7..3650 "
|
||||
"(CIS Kubernetes 1.2.17 recommends ≥30 days)."
|
||||
),
|
||||
)
|
||||
apiserver_strong_ciphers: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="Whitelist of strong TLS cipher suites required on the API server.",
|
||||
)
|
||||
kubelet_strong_ciphers: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="Whitelist of strong TLS cipher suites required on kubelet.",
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""M365 provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class M365ProviderConfig(ProviderConfigBase):
|
||||
# --- Entra (sign-in policy) ----------------------------------------
|
||||
sign_in_frequency: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=168,
|
||||
description=(
|
||||
"Hours between forced sign-ins for admin users. Range: 1..168 (1 h .. 7 days). "
|
||||
"Microsoft Conditional Access baseline for admin roles is ≤24 h."
|
||||
),
|
||||
)
|
||||
|
||||
# --- Teams ---------------------------------------------------------
|
||||
allowed_cloud_storage_services: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="External cloud storage services allowed in Teams.",
|
||||
)
|
||||
|
||||
# --- Exchange ------------------------------------------------------
|
||||
recommended_mailtips_large_audience_threshold: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=5,
|
||||
le=10000,
|
||||
description=(
|
||||
"Recipient count that should trigger a 'large audience' MailTip. "
|
||||
"Range: 5..10000 (Microsoft default 25)."
|
||||
),
|
||||
)
|
||||
|
||||
# --- Defender malware policy --------------------------------------
|
||||
default_recommended_extensions: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="File extensions blocked by the malware policy.",
|
||||
)
|
||||
|
||||
# --- Mailbox auditing ---------------------------------------------
|
||||
audit_log_age: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=3650,
|
||||
description=(
|
||||
"Days mailbox audit logs must be retained. Range: 30..3650 "
|
||||
"(M365 E3 default is 90 days; SEC/FINRA require ≥7 years)."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""MongoDB Atlas provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class MongoDBAtlasProviderConfig(ProviderConfigBase):
|
||||
"""MongoDB Atlas provider configuration schema.
|
||||
|
||||
Defines optional configuration parameters for MongoDB Atlas security checks,
|
||||
including service account secret validity constraints.
|
||||
"""
|
||||
|
||||
max_service_account_secret_validity_hours: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=720,
|
||||
description=(
|
||||
"Max hours a service account secret can stay valid. "
|
||||
"Range: 1..720 (1 h .. 30 days)."
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Mapping of provider name to its Pydantic schema class.
|
||||
|
||||
Kept in its own module so the validator stays free of provider-schema imports
|
||||
and callers pay the import cost only when they actually need the registry.
|
||||
"""
|
||||
|
||||
from prowler.config.schema.aws import AWSProviderConfig
|
||||
from prowler.config.schema.azure import AzureProviderConfig
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
from prowler.config.schema.cloudflare import CloudflareProviderConfig
|
||||
from prowler.config.schema.gcp import GCPProviderConfig
|
||||
from prowler.config.schema.github import GitHubProviderConfig
|
||||
from prowler.config.schema.kubernetes import KubernetesProviderConfig
|
||||
from prowler.config.schema.m365 import M365ProviderConfig
|
||||
from prowler.config.schema.mongodbatlas import MongoDBAtlasProviderConfig
|
||||
from prowler.config.schema.vercel import VercelProviderConfig
|
||||
|
||||
SCHEMAS: dict[str, type[ProviderConfigBase]] = {
|
||||
"aws": AWSProviderConfig,
|
||||
"azure": AzureProviderConfig,
|
||||
"gcp": GCPProviderConfig,
|
||||
"kubernetes": KubernetesProviderConfig,
|
||||
"m365": M365ProviderConfig,
|
||||
"github": GitHubProviderConfig,
|
||||
"mongodbatlas": MongoDBAtlasProviderConfig,
|
||||
"cloudflare": CloudflareProviderConfig,
|
||||
"vercel": VercelProviderConfig,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
def validate_provider_config(
|
||||
provider: str,
|
||||
raw: Any,
|
||||
schema_cls: type[ProviderConfigBase] | None,
|
||||
) -> dict:
|
||||
"""Validate a provider's config dict against its Pydantic schema.
|
||||
|
||||
Behavior is intentionally lenient to preserve backwards compatibility:
|
||||
|
||||
- If ``raw`` is not a dict, return an empty dict (mirrors prior loader).
|
||||
- If no schema is registered for ``provider``, return ``raw`` untouched.
|
||||
- On validation errors, log one WARNING per offending field, DROP those
|
||||
keys from the result, and continue. Consumers fall back to their own
|
||||
hard-coded defaults via ``audit_config.get(key, default)``.
|
||||
- Coerced values (e.g. ``"180"`` -> ``180``) replace the user's input
|
||||
so that downstream checks never receive a wrongly-typed value.
|
||||
"""
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
|
||||
if schema_cls is None:
|
||||
return raw
|
||||
|
||||
try:
|
||||
model = schema_cls.model_validate(raw)
|
||||
return model.model_dump(exclude_unset=True)
|
||||
except ValidationError as exc:
|
||||
bad_keys: set[str] = set()
|
||||
for err in exc.errors():
|
||||
loc = err.get("loc") or ()
|
||||
if not loc:
|
||||
continue
|
||||
key = loc[0]
|
||||
if not isinstance(key, str):
|
||||
continue
|
||||
bad_keys.add(key)
|
||||
logger.warning(
|
||||
f"prowler.config[{provider}.{key}] = {raw.get(key)!r} is invalid "
|
||||
f"({err.get('msg', 'validation error')}); the value will be ignored "
|
||||
f"and the built-in default will be used."
|
||||
)
|
||||
|
||||
cleaned = {k: v for k, v in raw.items() if k not in bad_keys}
|
||||
# Retry validation with the cleaned dict. Dropping invalid keys handles
|
||||
# common field-level mismatches, but revalidation can still fail due to
|
||||
# higher-level structural constraints (e.g. nested validation errors not
|
||||
# captured in the top-level bad_keys). In that case, log and return the
|
||||
# cleaned dict so consumers fall back to their own defaults.
|
||||
try:
|
||||
model = schema_cls.model_validate(cleaned)
|
||||
return model.model_dump(exclude_unset=True)
|
||||
except ValidationError as exc2:
|
||||
logger.error(
|
||||
f"prowler.config[{provider}] could not be revalidated after dropping "
|
||||
f"invalid keys ({bad_keys}); passing through the cleaned dict as-is. "
|
||||
f"Underlying errors: {exc2.errors()}"
|
||||
)
|
||||
return cleaned
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Reusable field validators shared across provider config schemas.
|
||||
|
||||
These are factored out so multiple providers can reuse the same validation
|
||||
logic (version strings, port ranges, IP/CIDR entries) instead of duplicating
|
||||
it per schema. Each validator accepts ``None`` so optional fields stay valid
|
||||
when the key is absent.
|
||||
"""
|
||||
|
||||
from ipaddress import ip_network
|
||||
from typing import Callable, Optional
|
||||
|
||||
_VERSION_PART_LABELS = ("X", "Y", "Z", "W")
|
||||
|
||||
|
||||
def make_dotted_version_validator(
|
||||
min_parts: int, max_parts: int
|
||||
) -> Callable[[Optional[str]], Optional[str]]:
|
||||
"""Build a validator for dotted numeric version strings.
|
||||
|
||||
The returned validator accepts ``None`` and strings made of between
|
||||
``min_parts`` and ``max_parts`` dot-separated numeric components. Anything
|
||||
else raises ``ValueError``.
|
||||
|
||||
Examples:
|
||||
``make_dotted_version_validator(3, 3)`` accepts ``"1.4.0"`` (semver).
|
||||
``make_dotted_version_validator(2, 2)`` accepts ``"1.28"`` (EKS minor).
|
||||
``make_dotted_version_validator(1, 2)`` accepts ``"17"`` or ``"8.2"``.
|
||||
"""
|
||||
if min_parts == max_parts:
|
||||
expected = ".".join(_VERSION_PART_LABELS[:min_parts])
|
||||
else:
|
||||
expected = " or ".join(
|
||||
f"'{'.'.join(_VERSION_PART_LABELS[:n])}'"
|
||||
for n in range(min_parts, max_parts + 1)
|
||||
)
|
||||
|
||||
def _validate(v: Optional[str]) -> Optional[str]:
|
||||
if v is None:
|
||||
return v
|
||||
parts = v.split(".")
|
||||
if not (min_parts <= len(parts) <= max_parts) or not all(
|
||||
p.isdigit() for p in parts
|
||||
):
|
||||
raise ValueError(f"{v!r} is not a valid version (expected {expected})")
|
||||
return v
|
||||
|
||||
return _validate
|
||||
|
||||
|
||||
def validate_port_range(v: Optional[list[int]]) -> Optional[list[int]]:
|
||||
"""Reject ports outside the valid ``1..65535`` range."""
|
||||
if v is None:
|
||||
return v
|
||||
for port in v:
|
||||
if not 1 <= port <= 65535:
|
||||
raise ValueError(f"port {port} is outside the valid range 1..65535")
|
||||
return v
|
||||
|
||||
|
||||
def validate_ip_networks(v: Optional[list[str]]) -> Optional[list[str]]:
|
||||
"""Reject entries that are not a valid IP address or CIDR network."""
|
||||
if v is None:
|
||||
return v
|
||||
for entry in v:
|
||||
try:
|
||||
ip_network(entry, strict=False)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"entry {entry!r} is not a valid IP or CIDR ({exc})"
|
||||
) from exc
|
||||
return v
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Vercel provider config schema with safety bounds."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class VercelProviderConfig(ProviderConfigBase):
|
||||
"""Vercel provider configuration schema.
|
||||
|
||||
Defines optional configuration parameters for Vercel security checks,
|
||||
including deployment branch policies, credential staleness thresholds,
|
||||
RBAC ownership limits, and secret detection patterns.
|
||||
"""
|
||||
|
||||
stable_branches: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="Branches considered stable for production deployments.",
|
||||
)
|
||||
days_to_expire_threshold: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=365,
|
||||
description=(
|
||||
"Days before token/certificate expiration to flag. Range: 7..365 "
|
||||
"(PCI-DSS 4.2.1.1: alert ≥30 days before expiry)."
|
||||
),
|
||||
)
|
||||
stale_token_threshold_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=30,
|
||||
le=3650,
|
||||
description=(
|
||||
"Days of inactivity before a token is considered stale. Range: 30..3650 "
|
||||
"(NIST AC-2(3) typical window 30..90 days)."
|
||||
),
|
||||
)
|
||||
stale_invitation_threshold_days: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=7,
|
||||
le=365,
|
||||
description=(
|
||||
"Days a pending invitation can stay open. Range: 7..365 "
|
||||
"(OWASP ASVS 2.7.1 recommends short-lived invitations)."
|
||||
),
|
||||
)
|
||||
max_owner_percentage: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=50,
|
||||
description=(
|
||||
"Max percentage of team members that can have the OWNER role. "
|
||||
"Range: 1..50 (PoLP — having >50% of a team as OWNER defeats RBAC; "
|
||||
"industry guidance recommends ≤25%)."
|
||||
),
|
||||
)
|
||||
max_owners: Optional[int] = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=1000,
|
||||
description="Absolute max owners (overrides percentage for large teams). Range: 1..1000.",
|
||||
)
|
||||
secret_suffixes: Optional[list[str]] = Field(
|
||||
default=None,
|
||||
description="Suffixes that mark a project env var as secret-like.",
|
||||
)
|
||||
@@ -797,6 +797,10 @@ def execute(
|
||||
is_finding_muted_args["org_domain"] = (
|
||||
global_provider.identity.org_domain
|
||||
)
|
||||
elif global_provider.type == "linode":
|
||||
is_finding_muted_args["account_id"] = (
|
||||
global_provider.identity.account_id
|
||||
)
|
||||
elif not is_builtin_provider(global_provider.type):
|
||||
# External/custom provider — delegate identity args
|
||||
is_finding_muted_args = global_provider.get_mutelist_finding_args()
|
||||
|
||||
@@ -1106,6 +1106,37 @@ class CheckReportCloudflare(Check_Report):
|
||||
return "global"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportLinode(Check_Report):
|
||||
"""Contains the Linode Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
region: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
metadata: Dict,
|
||||
resource: Any,
|
||||
resource_name: str,
|
||||
resource_id: str,
|
||||
region: str = "global",
|
||||
) -> None:
|
||||
"""Initialize the Linode Check's finding information.
|
||||
|
||||
Args:
|
||||
metadata: The metadata of the check.
|
||||
resource: Basic information about the resource.
|
||||
resource_name: The name of the resource related with the finding.
|
||||
resource_id: The id of the resource related with the finding.
|
||||
region: The region of the resource related with the finding.
|
||||
"""
|
||||
super().__init__(metadata, resource)
|
||||
self.resource_name = resource_name
|
||||
self.resource_id = resource_id
|
||||
self.region = region
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportM365(Check_Report):
|
||||
"""Contains the M365 Check's finding information."""
|
||||
|
||||
@@ -51,6 +51,7 @@ class ProwlerArgumentParser:
|
||||
"okta",
|
||||
"scaleway",
|
||||
"stackit",
|
||||
"linode",
|
||||
}
|
||||
all_providers = set(Provider.get_available_providers())
|
||||
new_providers = sorted(all_providers - known_providers)
|
||||
@@ -73,10 +74,10 @@ class ProwlerArgumentParser:
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog="prowler",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...",
|
||||
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,dashboard,iac,image,llm{extra_providers_csv}}} ...",
|
||||
epilog=f"""
|
||||
Available Cloud Providers:
|
||||
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel{extra_providers_csv}}}
|
||||
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode{extra_providers_csv}}}
|
||||
aws AWS Provider
|
||||
azure Azure Provider
|
||||
gcp GCP Provider
|
||||
@@ -96,7 +97,8 @@ Available Cloud Providers:
|
||||
nhn NHN Provider (Unofficial)
|
||||
mongodbatlas MongoDB Atlas Provider
|
||||
scaleway Scaleway Provider
|
||||
vercel Vercel Provider{extra_providers_text}
|
||||
vercel Vercel Provider
|
||||
linode Linode Provider{extra_providers_text}
|
||||
|
||||
|
||||
Available components:
|
||||
|
||||
@@ -22,11 +22,14 @@ def get_asd_essential_eight_table(
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
section_seen = {}
|
||||
provider = ""
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "ASD-Essential-Eight":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
@@ -36,21 +39,33 @@ def get_asd_essential_eight_table(
|
||||
"PASS": 0,
|
||||
"Muted": 0,
|
||||
}
|
||||
section_seen[section] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-section counts: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_seen[section]:
|
||||
section_seen[section].add(index)
|
||||
if finding.muted:
|
||||
sections[section]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
asd_essential_eight_compliance_table["Provider"].append(compliance.Provider)
|
||||
asd_essential_eight_compliance_table["Provider"].append(provider)
|
||||
asd_essential_eight_compliance_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
asd_essential_eight_compliance_table["Status"].append(
|
||||
|
||||
@@ -22,33 +22,47 @@ def get_c5_table(
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
sections = {}
|
||||
section_seen = {}
|
||||
provider = ""
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "C5":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
|
||||
if section not in sections:
|
||||
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
section_seen[section] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-section counts: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_seen[section]:
|
||||
section_seen[section].add(index)
|
||||
if finding.muted:
|
||||
sections[section]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
section_table["Provider"].append(compliance.Provider)
|
||||
section_table["Provider"].append(provider)
|
||||
section_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
section_table["Status"].append(
|
||||
|
||||
@@ -22,33 +22,47 @@ def get_ccc_table(
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
sections = {}
|
||||
section_seen = {}
|
||||
provider = ""
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "CCC":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
|
||||
if section not in sections:
|
||||
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
section_seen[section] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-section counts: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_seen[section]:
|
||||
section_seen[section].add(index)
|
||||
if finding.muted:
|
||||
sections[section]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
section_table["Provider"].append(compliance.Provider)
|
||||
section_table["Provider"].append(provider)
|
||||
section_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
section_table["Status"].append(
|
||||
|
||||
@@ -13,6 +13,9 @@ def get_cis_table(
|
||||
compliance_overview: bool,
|
||||
):
|
||||
sections = {}
|
||||
section_muted_seen = {}
|
||||
section_split_seen = {}
|
||||
provider = ""
|
||||
cis_compliance_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
@@ -29,6 +32,7 @@ def get_cis_table(
|
||||
for compliance in check_compliances:
|
||||
version_in_name = compliance_framework.split("_")[1]
|
||||
if compliance.Framework == "CIS" and version_in_name in compliance.Version:
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
@@ -40,9 +44,19 @@ def get_cis_table(
|
||||
"Level 2": {"FAIL": 0, "PASS": 0},
|
||||
"Muted": 0,
|
||||
}
|
||||
section_muted_seen[section] = set()
|
||||
section_split_seen[section] = {
|
||||
"Level 1": set(),
|
||||
"Level 2": set(),
|
||||
}
|
||||
if finding.muted:
|
||||
# Overview total: count each finding once per framework
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
# Per-section Muted: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_muted_seen[section]:
|
||||
section_muted_seen[section].add(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
@@ -50,13 +64,21 @@ def get_cis_table(
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
if "Level 1" in attribute.Profile:
|
||||
if not finding.muted:
|
||||
if (
|
||||
not finding.muted
|
||||
and index not in section_split_seen[section]["Level 1"]
|
||||
):
|
||||
section_split_seen[section]["Level 1"].add(index)
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 1"]["FAIL"] += 1
|
||||
else:
|
||||
sections[section]["Level 1"]["PASS"] += 1
|
||||
elif "Level 2" in attribute.Profile:
|
||||
if not finding.muted:
|
||||
if (
|
||||
not finding.muted
|
||||
and index not in section_split_seen[section]["Level 2"]
|
||||
):
|
||||
section_split_seen[section]["Level 2"].add(index)
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 2"]["FAIL"] += 1
|
||||
else:
|
||||
@@ -65,7 +87,7 @@ def get_cis_table(
|
||||
# Add results to table
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
cis_compliance_table["Provider"].append(compliance.Provider)
|
||||
cis_compliance_table["Provider"].append(provider)
|
||||
cis_compliance_table["Section"].append(section)
|
||||
if sections[section]["Level 1"]["FAIL"] > 0:
|
||||
cis_compliance_table["Level 1"].append(
|
||||
|
||||
@@ -13,6 +13,8 @@ def get_ens_table(
|
||||
compliance_overview: bool,
|
||||
):
|
||||
marcos = {}
|
||||
marco_muted_seen = {}
|
||||
provider = ""
|
||||
ens_compliance_table = {
|
||||
"Proveedor": [],
|
||||
"Marco/Categoria": [],
|
||||
@@ -31,6 +33,7 @@ def get_ens_table(
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "ENS":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
marco_categoria = f"{attribute.Marco}/{attribute.Categoria}"
|
||||
@@ -44,17 +47,23 @@ def get_ens_table(
|
||||
"Bajo": 0,
|
||||
"Muted": 0,
|
||||
}
|
||||
marco_muted_seen[marco_categoria] = set()
|
||||
if finding.muted:
|
||||
# Overview total: count each finding once per framework
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
# Per-marco Muted: count each finding once per marco
|
||||
# it belongs to (a finding can map to several marcos).
|
||||
if index not in marco_muted_seen[marco_categoria]:
|
||||
marco_muted_seen[marco_categoria].add(index)
|
||||
marcos[marco_categoria]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL":
|
||||
if (
|
||||
attribute.Tipo != "recomendacion"
|
||||
and index not in fail_count
|
||||
):
|
||||
fail_count.append(index)
|
||||
if attribute.Tipo != "recomendacion":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
# Mark every marco the finding belongs to as
|
||||
# NO CUMPLE, not just the first one seen.
|
||||
marcos[marco_categoria][
|
||||
"Estado"
|
||||
] = f"{Fore.RED}NO CUMPLE{Style.RESET_ALL}"
|
||||
@@ -71,7 +80,7 @@ def get_ens_table(
|
||||
|
||||
# Add results to table
|
||||
for marco in sorted(marcos):
|
||||
ens_compliance_table["Proveedor"].append(compliance.Provider)
|
||||
ens_compliance_table["Proveedor"].append(provider)
|
||||
ens_compliance_table["Marco/Categoria"].append(marco)
|
||||
ens_compliance_table["Estado"].append(marcos[marco]["Estado"])
|
||||
ens_compliance_table["Opcional"].append(
|
||||
|
||||
@@ -13,7 +13,9 @@ def get_kisa_ismsp_table(
|
||||
compliance_overview: bool,
|
||||
):
|
||||
sections = {}
|
||||
section_seen = {}
|
||||
sections_status = {}
|
||||
provider = ""
|
||||
kisa_ismsp_compliance_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
@@ -31,6 +33,7 @@ def get_kisa_ismsp_table(
|
||||
compliance.Framework.startswith("KISA")
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
@@ -43,16 +46,28 @@ def get_kisa_ismsp_table(
|
||||
},
|
||||
"Muted": 0,
|
||||
}
|
||||
section_seen[section] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["Status"]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-section counts: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_seen[section]:
|
||||
section_seen[section].add(index)
|
||||
if finding.muted:
|
||||
sections[section]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
sections[section]["Status"]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
sections[section]["Status"]["PASS"] += 1
|
||||
|
||||
# Add results to table
|
||||
@@ -70,7 +85,7 @@ def get_kisa_ismsp_table(
|
||||
else:
|
||||
sections_status[section] = f"{Fore.GREEN}PASS{Style.RESET_ALL}"
|
||||
for section in sections:
|
||||
kisa_ismsp_compliance_table["Provider"].append(compliance.Provider)
|
||||
kisa_ismsp_compliance_table["Provider"].append(provider)
|
||||
kisa_ismsp_compliance_table["Section"].append(section)
|
||||
kisa_ismsp_compliance_table["Status"].append(sections_status[section])
|
||||
kisa_ismsp_compliance_table["Muted"].append(
|
||||
|
||||
@@ -13,6 +13,8 @@ def get_mitre_attack_table(
|
||||
compliance_overview: bool,
|
||||
):
|
||||
tactics = {}
|
||||
tactic_seen = {}
|
||||
provider = ""
|
||||
mitre_compliance_table = {
|
||||
"Provider": [],
|
||||
"Tactic": [],
|
||||
@@ -30,27 +32,38 @@ def get_mitre_attack_table(
|
||||
"MITRE-ATTACK" in compliance.Framework
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for tactic in requirement.Tactics:
|
||||
if tactic not in tactics:
|
||||
tactics[tactic] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
tactic_seen[tactic] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-tactic counts: count each finding once per tactic
|
||||
# it belongs to (a finding can map to several tactics).
|
||||
if index not in tactic_seen[tactic]:
|
||||
tactic_seen[tactic].add(index)
|
||||
if finding.muted:
|
||||
tactics[tactic]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
tactics[tactic]["FAIL"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
tactics[tactic]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
tactics[tactic]["PASS"] += 1
|
||||
tactics[tactic]["PASS"] += 1
|
||||
# Add results to table
|
||||
tactics = dict(sorted(tactics.items()))
|
||||
for tactic in tactics:
|
||||
mitre_compliance_table["Provider"].append(compliance.Provider)
|
||||
mitre_compliance_table["Provider"].append(provider)
|
||||
mitre_compliance_table["Tactic"].append(tactic)
|
||||
if tactics[tactic]["FAIL"] > 0:
|
||||
mitre_compliance_table["Status"].append(
|
||||
|
||||
@@ -22,33 +22,47 @@ def get_okta_idaas_stig_table(
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
sections = {}
|
||||
section_seen = {}
|
||||
provider = ""
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "Okta-IDaaS-STIG":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
|
||||
if section not in sections:
|
||||
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
section_seen[section] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-section counts: count each finding once per section
|
||||
# it belongs to (a finding can map to several sections).
|
||||
if index not in section_seen[section]:
|
||||
section_seen[section].add(index)
|
||||
if finding.muted:
|
||||
sections[section]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
section_table["Provider"].append(compliance.Provider)
|
||||
section_table["Provider"].append(provider)
|
||||
section_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
section_table["Status"].append(
|
||||
|
||||
@@ -24,6 +24,8 @@ def get_prowler_threatscore_table(
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
pillars = {}
|
||||
pillar_seen = {}
|
||||
provider = ""
|
||||
generic_score = 0
|
||||
max_generic_score = 0
|
||||
counted_findings_generic = []
|
||||
@@ -35,6 +37,7 @@ def get_prowler_threatscore_table(
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "ProwlerThreatScore":
|
||||
provider = compliance.Provider
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
pillar = attribute.Section
|
||||
@@ -65,17 +68,28 @@ def get_prowler_threatscore_table(
|
||||
|
||||
if pillar not in pillars:
|
||||
pillars[pillar] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
pillar_seen[pillar] = set()
|
||||
|
||||
# Overview totals: count each finding once per framework
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
pillars[pillar]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
elif finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
pillars[pillar]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
|
||||
# Per-pillar counts: count each finding once per pillar
|
||||
# it belongs to (a finding can map to several pillars).
|
||||
if index not in pillar_seen[pillar]:
|
||||
pillar_seen[pillar].add(index)
|
||||
if finding.muted:
|
||||
pillars[pillar]["Muted"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
pillars[pillar]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
pillars[pillar]["PASS"] += 1
|
||||
|
||||
# Generic score
|
||||
@@ -90,18 +104,21 @@ def get_prowler_threatscore_table(
|
||||
counted_findings_generic.append(index)
|
||||
|
||||
no_findings_pillars = []
|
||||
bulk_compliance = Compliance.get_bulk(provider=compliance.Provider.lower()).get(
|
||||
compliance_framework
|
||||
bulk_compliance = (
|
||||
Compliance.get_bulk(provider=provider.lower()).get(compliance_framework)
|
||||
if provider
|
||||
else None
|
||||
)
|
||||
for requirement in bulk_compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
pillar = attribute.Section
|
||||
if pillar not in pillars.keys() and pillar not in no_findings_pillars:
|
||||
no_findings_pillars.append(pillar)
|
||||
if bulk_compliance:
|
||||
for requirement in bulk_compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
pillar = attribute.Section
|
||||
if pillar not in pillars.keys() and pillar not in no_findings_pillars:
|
||||
no_findings_pillars.append(pillar)
|
||||
|
||||
pillars = dict(sorted(pillars.items()))
|
||||
for pillar in pillars:
|
||||
pillar_table["Provider"].append(compliance.Provider)
|
||||
pillar_table["Provider"].append(provider)
|
||||
pillar_table["Pillar"].append(pillar)
|
||||
if max_score_per_pillar[pillar] == 0:
|
||||
pillar_score = 100.0
|
||||
@@ -127,7 +144,7 @@ def get_prowler_threatscore_table(
|
||||
)
|
||||
|
||||
for pillar in no_findings_pillars:
|
||||
pillar_table["Provider"].append(compliance.Provider)
|
||||
pillar_table["Provider"].append(provider)
|
||||
pillar_table["Pillar"].append(pillar)
|
||||
pillar_table["Score"].append(f"{Style.BRIGHT}{Fore.GREEN}100%{Style.RESET_ALL}")
|
||||
pillar_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user