- Prowler is the Open Cloud Security Platform trusted by thousands to automate security and compliance in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
+ Prowler is the Open Cloud Security Platform trusted by thousands to automate security and compliance in any cloud environment. With thousands of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
-Secure ANY cloud at AI Speed at prowler.com
+The Agentic Cloud Defender
+
"
+ response = httpx.Response(
+ 404,
+ request=httpx.Request("GET", "https://provider.example/v1/models"),
+ )
+
+ with patch("tasks.jobs.lighthouse_providers.openai.OpenAI") as mock_openai:
+ mock_client = MagicMock()
+ mock_client.models.list.side_effect = openai.NotFoundError(
+ remote_body,
+ response=response,
+ body=remote_body,
+ )
+ mock_openai.return_value = mock_client
+
+ eager_result = refresh_lighthouse_provider_models_task.apply(
+ kwargs={
+ "provider_config_id": str(provider_cfg.id),
+ "tenant_id": str(tenants_fixture[0].id),
+ }
+ )
+
+ assert eager_result.successful()
+ result = eager_result.result
+ assert result["created"] == 0
+ assert result["updated"] == 0
+ assert result["deleted"] == 0
+ assert result["error"] == "Provider connection failed"
+ assert remote_body not in result["error"]
+
def test_refresh_models_mixed_operations(self, tenants_fixture):
"""Test mixed create, update, and delete operations."""
# Create provider configuration
@@ -3042,6 +3422,7 @@ class TestTaskTimeLimits:
for name in (
"scan-perform",
"scan-perform-scheduled",
+ "attack-paths-scan-perform",
"provider-deletion",
"tenant-deletion",
):
diff --git a/api/uv.lock b/api/uv.lock
index 04a6ce76ef..604e3bc35c 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -16,7 +16,7 @@ constraints = [
{ name = "aiobotocore", specifier = "==2.25.1" },
{ name = "aiofiles", specifier = "==24.1.0" },
{ name = "aiohappyeyeballs", specifier = "==2.6.1" },
- { name = "aiohttp", specifier = "==3.14.0" },
+ { name = "aiohttp", specifier = "==3.14.3" },
{ name = "aioitertools", specifier = "==0.13.0" },
{ name = "aiosignal", specifier = "==1.4.0" },
{ name = "alibabacloud-actiontrail20200706", specifier = "==2.4.1" },
@@ -45,7 +45,7 @@ constraints = [
{ name = "alibabacloud-sls20201230", specifier = "==5.9.0" },
{ name = "alibabacloud-sts20150401", specifier = "==1.1.6" },
{ name = "alibabacloud-tea", specifier = "==0.4.3" },
- { name = "alibabacloud-tea-openapi", specifier = "==0.4.4" },
+ { name = "alibabacloud-tea-openapi", specifier = "==0.4.5" },
{ name = "alibabacloud-tea-util", specifier = "==0.3.14" },
{ name = "alibabacloud-tea-xml", specifier = "==0.0.3" },
{ name = "alibabacloud-vpc20160428", specifier = "==6.13.0" },
@@ -127,9 +127,9 @@ constraints = [
{ name = "coverage", specifier = "==7.5.4" },
{ name = "cron-descriptor", specifier = "==1.4.5" },
{ name = "crowdstrike-falconpy", specifier = "==1.6.0" },
- { name = "cryptography", specifier = "==46.0.7" },
+ { name = "cryptography", specifier = "==50.0.0" },
{ name = "cycler", specifier = "==0.12.1" },
- { name = "darabonba-core", specifier = "==1.0.5" },
+ { name = "darabonba-core", specifier = "==1.0.8" },
{ name = "dash", specifier = "==3.1.1" },
{ name = "dash-bootstrap-components", specifier = "==2.0.3" },
{ name = "debugpy", specifier = "==1.8.20" },
@@ -194,7 +194,7 @@ constraints = [
{ name = "h2", specifier = "==4.3.0" },
{ name = "hpack", specifier = "==4.1.0" },
{ name = "httpcore", specifier = "==1.0.9" },
- { name = "httplib2", specifier = "==0.31.2" },
+ { name = "httplib2", specifier = "==0.32.0" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "humanfriendly", specifier = "==10.0" },
{ name = "hyperframe", specifier = "==6.1.0" },
@@ -231,13 +231,13 @@ constraints = [
{ name = "matplotlib", specifier = "==3.10.8" },
{ name = "mccabe", specifier = "==0.7.0" },
{ name = "mdurl", specifier = "==0.1.2" },
- { name = "microsoft-kiota-abstractions", specifier = "==1.9.9" },
- { name = "microsoft-kiota-authentication-azure", specifier = "==1.9.9" },
- { name = "microsoft-kiota-http", specifier = "==1.9.9" },
- { name = "microsoft-kiota-serialization-form", specifier = "==1.9.9" },
- { name = "microsoft-kiota-serialization-json", specifier = "==1.9.9" },
- { name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.9" },
- { name = "microsoft-kiota-serialization-text", specifier = "==1.9.9" },
+ { name = "microsoft-kiota-abstractions", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-http", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-form", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-json", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
{ name = "microsoft-security-utilities-secret-masker", specifier = "==1.0.0b4" },
{ name = "msal", specifier = "==1.35.0b1" },
{ name = "msal-extensions", specifier = "==1.2.0" },
@@ -254,7 +254,7 @@ constraints = [
{ name = "nltk", specifier = "==3.9.4" },
{ name = "numpy", specifier = "==2.2.6" },
{ name = "oauthlib", specifier = "==3.3.1" },
- { name = "oci", specifier = "==2.169.0" },
+ { name = "oci", specifier = "==2.183.0" },
{ name = "openai", specifier = "==1.109.1" },
{ name = "openstacksdk", specifier = "==4.2.0" },
{ name = "opentelemetry-api", specifier = "==1.39.1" },
@@ -266,7 +266,7 @@ constraints = [
{ name = "pagerduty", specifier = "==6.1.0" },
{ name = "pandas", specifier = "==2.2.3" },
{ name = "pbr", specifier = "==7.0.3" },
- { name = "pillow", specifier = "==12.2.0" },
+ { name = "pillow", specifier = "==12.3.0" },
{ name = "pkginfo", specifier = "==1.12.1.2" },
{ name = "platformdirs", specifier = "==4.5.1" },
{ name = "plotly", specifier = "==6.5.2" },
@@ -282,8 +282,8 @@ constraints = [
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "py-deviceid", specifier = "==0.1.1" },
{ name = "py-iam-expand", specifier = "==0.3.0" },
- { name = "py-ocsf-models", specifier = "==0.8.1" },
- { name = "pyasn1", specifier = "==0.6.3" },
+ { name = "py-ocsf-models", specifier = "==0.10.0" },
+ { name = "pyasn1", specifier = "==0.6.4" },
{ name = "pyasn1-modules", specifier = "==0.4.2" },
{ name = "pycodestyle", specifier = "==2.14.0" },
{ name = "pycparser", specifier = "==3.0" },
@@ -295,7 +295,7 @@ constraints = [
{ name = "pylint", specifier = "==3.2.5" },
{ name = "pymsalruntime", specifier = "==0.18.1" },
{ name = "pynacl", specifier = "==1.6.2" },
- { name = "pyopenssl", specifier = "==26.0.0" },
+ { name = "pyopenssl", specifier = "==26.2.0" },
{ name = "pyparsing", specifier = "==3.3.2" },
{ name = "pyreadline3", specifier = "==3.5.4" },
{ name = "pysocks", specifier = "==1.7.1" },
@@ -364,7 +364,7 @@ constraints = [
{ name = "wcwidth", specifier = "==0.5.3" },
{ name = "websocket-client", specifier = "==1.9.0" },
{ name = "werkzeug", specifier = "==3.1.7" },
- { name = "workos", specifier = "==6.0.8" },
+ { name = "workos", specifier = "==8.3.0" },
{ name = "wrapt", specifier = "==1.17.3" },
{ name = "xlsxwriter", specifier = "==3.2.9" },
{ name = "xmlsec", specifier = "==1.3.17" },
@@ -377,8 +377,15 @@ constraints = [
]
overrides = [
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },
+ { name = "cryptography", specifier = "==50.0.0" },
{ name = "dulwich", specifier = "==1.2.5" },
- { name = "microsoft-kiota-abstractions", specifier = "==1.9.9" },
+ { name = "microsoft-kiota-abstractions", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-http", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-form", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-json", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-multipart", specifier = "==1.9.10" },
+ { name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" },
{ name = "okta", specifier = "==3.4.2" },
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.13.0" },
]
@@ -472,7 +479,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.14.0"
+version = "3.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -484,44 +491,44 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" },
- { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" },
- { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" },
- { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" },
- { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" },
- { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" },
- { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" },
- { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" },
- { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" },
- { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" },
- { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" },
- { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" },
- { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" },
- { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" },
- { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" },
- { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" },
- { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" },
- { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" },
- { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" },
- { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" },
- { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" },
- { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" },
- { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" },
- { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" },
- { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" },
- { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" },
- { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" },
- { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" },
- { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" },
- { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" },
- { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" },
- { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" },
- { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" },
- { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
+ { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
+ { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
+ { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
+ { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
+ { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
+ { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
+ { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
+ { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
+ { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
+ { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
]
[[package]]
@@ -853,7 +860,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0
[[package]]
name = "alibabacloud-tea-openapi"
-version = "0.4.4"
+version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alibabacloud-credentials" },
@@ -862,9 +869,9 @@ dependencies = [
{ name = "cryptography" },
{ name = "darabonba-core" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/30/93/138bcdc8fc596add73e37cf2073798f285284d1240bda9ee02f9384fc6be/alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce", size = 21960, upload-time = "2026-03-26T10:16:16.792Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/73/fb0c4d44759791ecdf269fc715c1e810fa1aba3981bfaaf8a01f61899296/alibabacloud_tea_openapi-0.4.5.tar.gz", hash = "sha256:75fa1f4360a46e41f5bf5f8d4917e52efb6f64885839bc1328c35590670c97b9", size = 26616, upload-time = "2026-07-14T13:15:39.364Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f5/5a/6bfc4506438c1809c486f66217ad11eab78157192b3d5707b4e2f4212f6c/alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f", size = 26236, upload-time = "2026-03-26T10:16:15.861Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ec/6b368a10e9c2e8b1b394c69b96ac213ae66e8c4895e0baa1ffaf7178fd32/alibabacloud_tea_openapi-0.4.5-py3-none-any.whl", hash = "sha256:338979095c7beda80a5b413c31262892cafdc12069dde4ce4fc2e4f7ce0fc609", size = 33333, upload-time = "2026-07-14T13:15:38.365Z" },
]
[[package]]
@@ -2088,6 +2095,37 @@ toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
+[[package]]
+name = "crc32c"
+version = "2.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/66/7e97aa77af7cf6afbff26e3651b564fe41932599bc2d3dce0b2f73d4829a/crc32c-2.8.tar.gz", hash = "sha256:578728964e59c47c356aeeedee6220e021e124b9d3e8631d95d9a5e5f06e261c", size = 48179, upload-time = "2025-10-17T06:20:13.61Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/0b/5e03b22d913698e9cc563f39b9f6bbd508606bf6b8e9122cd6bf196b87ea/crc32c-2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e560a97fbb96c9897cb1d9b5076ef12fc12e2e25622530a1afd0de4240f17e1f", size = 66329, upload-time = "2025-10-17T06:19:01.771Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/38/2fe0051ffe8c6a650c8b1ac0da31b8802d1dbe5fa40a84e4b6b6f5583db5/crc32c-2.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6762d276d90331a490ef7e71ffee53b9c0eb053bd75a272d786f3b08d3fe3671", size = 62988, upload-time = "2025-10-17T06:19:02.953Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/30/5837a71c014be83aba1469c58820d287fc836512a0cad6b8fdd43868accd/crc32c-2.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:60670569f5ede91e39f48fb0cb4060e05b8d8704dd9e17ede930bf441b2f73ef", size = 61522, upload-time = "2025-10-17T06:19:03.796Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/29/63972fc1452778e2092ae998c50cbfc2fc93e3fa9798a0278650cd6169c5/crc32c-2.8-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:711743da6ccc70b3c6718c328947b0b6f34a1fe6a6c27cc6c1d69cc226bf70e9", size = 80200, upload-time = "2025-10-17T06:19:04.617Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3a/60eb49d7bdada4122b3ffd45b0df54bdc1b8dd092cda4b069a287bdfcff4/crc32c-2.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eb4094a2054774f13b26f21bf56792bb44fa1fcee6c6ad099387a43ffbfb4fa", size = 81757, upload-time = "2025-10-17T06:19:05.496Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/63/6efc1b64429ef7d23bd58b75b7ac24d15df327e3ebbe9c247a0f7b1c2ed1/crc32c-2.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fff15bf2bd3e95780516baae935ed12be88deaa5ebe6143c53eb0d26a7bdc7b7", size = 80830, upload-time = "2025-10-17T06:19:06.621Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/eb/0ae9f436f8004f1c88f7429e659a7218a3879bd11a6b18ed1257aad7e98b/crc32c-2.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c0e11e3826668121fa53e0745635baf5e4f0ded437e8ff63ea56f38fc4f970a", size = 80095, upload-time = "2025-10-17T06:19:07.381Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/81/4afc9d468977a4cd94a2eb62908553345009a7c0d30e74463a15d4b48ec3/crc32c-2.8-cp311-cp311-win32.whl", hash = "sha256:38f915336715d1f1353ab07d7d786f8a789b119e273aea106ba55355dfc9101d", size = 64886, upload-time = "2025-10-17T06:19:08.497Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e8/94e839c9f7e767bf8479046a207afd440a08f5c59b52586e1af5e64fa4a0/crc32c-2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60e0a765b1caab8d31b2ea80840639253906a9351d4b861551c8c8625ea20f86", size = 66639, upload-time = "2025-10-17T06:19:09.338Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/36/fd18ef23c42926b79c7003e16cb0f79043b5b179c633521343d3b499e996/crc32c-2.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:572ffb1b78cce3d88e8d4143e154d31044a44be42cb3f6fbbf77f1e7a941c5ab", size = 66379, upload-time = "2025-10-17T06:19:10.115Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b8/c584958e53f7798dd358f5bdb1bbfc97483134f053ee399d3eeb26cca075/crc32c-2.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cf827b3758ee0c4aacd21ceca0e2da83681f10295c38a10bfeb105f7d98f7a68", size = 63042, upload-time = "2025-10-17T06:19:10.946Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e6/6f2af0ec64a668a46c861e5bc778ea3ee42171fedfc5440f791f470fd783/crc32c-2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:106fbd79013e06fa92bc3b51031694fcc1249811ed4364ef1554ee3dd2c7f5a2", size = 61528, upload-time = "2025-10-17T06:19:11.768Z" },
+ { url = "https://files.pythonhosted.org/packages/17/8b/4a04bd80a024f1a23978f19ae99407783e06549e361ab56e9c08bba3c1d3/crc32c-2.8-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6dde035f91ffbfe23163e68605ee5a4bb8ceebd71ed54bb1fb1d0526cdd125a2", size = 80028, upload-time = "2025-10-17T06:19:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/21/8f/01c7afdc76ac2007d0e6a98e7300b4470b170480f8188475b597d1f4b4c6/crc32c-2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e41ebe7c2f0fdcd9f3a3fd206989a36b460b4d3f24816d53e5be6c7dba72c5e1", size = 81531, upload-time = "2025-10-17T06:19:13.406Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2b/8f78c5a8cc66486be5f51b6f038fc347c3ba748d3ea68be17a014283c331/crc32c-2.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecf66cf90266d9c15cea597d5cc86c01917cd1a238dc3c51420c7886fa750d7e", size = 80608, upload-time = "2025-10-17T06:19:14.223Z" },
+ { url = "https://files.pythonhosted.org/packages/db/86/fad1a94cdeeeb6b6e2323c87f970186e74bfd6fbfbc247bf5c88ad0873d5/crc32c-2.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59eee5f3a69ad0793d5fa9cdc9b9d743b0cd50edf7fccc0a3988a821fef0208c", size = 79886, upload-time = "2025-10-17T06:19:15.345Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/db/1a7cb6757a1e32376fa2dfce00c815ea4ee614a94f9bff8228e37420c183/crc32c-2.8-cp312-cp312-win32.whl", hash = "sha256:a73d03ce3604aa5d7a2698e9057a0eef69f529c46497b27ee1c38158e90ceb76", size = 64896, upload-time = "2025-10-17T06:19:16.457Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/8e/2024de34399b2e401a37dcb54b224b56c747b0dc46de4966886827b4d370/crc32c-2.8-cp312-cp312-win_amd64.whl", hash = "sha256:56b3b7d015247962cf58186e06d18c3d75a1a63d709d3233509e1c50a2d36aa2", size = 66645, upload-time = "2025-10-17T06:19:17.235Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/1d/dd926c68eb8aac8b142a1a10b8eb62d95212c1cf81775644373fe7cceac2/crc32c-2.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5833f4071da7ea182c514ba17d1eee8aec3c5be927d798222fbfbbd0f5eea02c", size = 62345, upload-time = "2025-10-17T06:20:09.39Z" },
+ { url = "https://files.pythonhosted.org/packages/51/be/803404e5abea2ef2c15042edca04bbb7f625044cca879e47f186b43887c2/crc32c-2.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1dc4da036126ac07b39dd9d03e93e585ec615a2ad28ff12757aef7de175295a8", size = 61229, upload-time = "2025-10-17T06:20:10.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/3a/00cc578cd27ed0b22c9be25cef2c24539d92df9fa80ebd67a3fc5419724c/crc32c-2.8-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15905fa78344654e241371c47e6ed2411f9eeb2b8095311c68c88eccf541e8b4", size = 64108, upload-time = "2025-10-17T06:20:11.072Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/bc/0587ef99a1c7629f95dd0c9d4f3d894de383a0df85831eb16c48a6afdae4/crc32c-2.8-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c596f918688821f796434e89b431b1698396c38bf0b56de873621528fe3ecb1e", size = 64815, upload-time = "2025-10-17T06:20:11.919Z" },
+ { url = "https://files.pythonhosted.org/packages/73/42/94f2b8b92eae9064fcfb8deef2b971514065bd606231f8857ff8ae02bebd/crc32c-2.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8d23c4fe01b3844cb6e091044bc1cebdef7d16472e058ce12d9fadf10d2614af", size = 66659, upload-time = "2025-10-17T06:20:12.766Z" },
+]
+
[[package]]
name = "cron-descriptor"
version = "1.4.5"
@@ -2112,47 +2150,45 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.7"
+version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
- { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
- { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
- { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
- { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
- { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
- { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
- { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
- { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
- { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
- { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
- { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
- { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
- { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
- { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
- { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
- { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
- { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
- { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
- { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
- { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
- { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
- { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
- { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
- { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
- { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
- { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
- { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
- { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
- { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
+ { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
+ { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
+ { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
+ { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
+ { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
+ { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
+ { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
+ { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
+ { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
+ { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
+ { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
]
[[package]]
@@ -2166,15 +2202,17 @@ wheels = [
[[package]]
name = "darabonba-core"
-version = "1.0.5"
+version = "1.0.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "alibabacloud-tea" },
{ name = "requests" },
+ { name = "websocket-client" },
]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/83/9321ccdb7a800c2cb97d8fa34bead5f20141f27f804594fd1fd815c4cd07/darabonba_core-1.0.8.tar.gz", hash = "sha256:f1661960b368e342d3d36434be82d264b70a01c49e843921d8a4dacd217376ae", size = 27604, upload-time = "2026-07-13T02:07:34.093Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/66/d3/a7daaee544c904548e665829b51a9fa2572acb82c73ad787a8ff90273002/darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c", size = 24580, upload-time = "2025-12-12T07:53:59.494Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/88/38800ca22f39a31fdb75c7b2867c61d3af5e2792cee0b72942a639c88a79/darabonba_core-1.0.8-py3-none-any.whl", hash = "sha256:ac093fdd40f88f2f9dfbbbfd7bc143495a3cb031f35b397c98d24edfa6b69483", size = 30957, upload-time = "2026-07-13T02:07:33.138Z" },
]
[[package]]
@@ -3327,14 +3365,14 @@ wheels = [
[[package]]
name = "httplib2"
-version = "0.31.2"
+version = "0.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" },
+ { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" },
]
[[package]]
@@ -3357,6 +3395,134 @@ http2 = [
{ name = "h2" },
]
+[[package]]
+name = "huaweicloudsdkcore"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "defusedxml" },
+ { name = "pyasn1" },
+ { name = "pymongo" },
+ { name = "pyyaml" },
+ { name = "requests-toolbelt" },
+ { name = "simplejson" },
+ { name = "six" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/f5/65e90764ea3bbfef50fb68cd5e12340acf1f51e9276b11745fbf5feb7e0e/huaweicloudsdkcore-3.1.204-py3-none-any.whl", hash = "sha256:9ae17744795ebdc8ce9291373a3a27bf72e90aa98677cfce0ea9394376875a95", size = 69578, upload-time = "2026-07-09T09:01:59.715Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkcts"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/32/d06328e35375d4aa606719a27856cdf57b1f7fb0c49d4dfd22a9609dba19/huaweicloudsdkcts-3.1.204-py3-none-any.whl", hash = "sha256:9def561aa784a6ee13b46bfc96888cd1df5bfc42f8a89e60b42c91c608bf6d60", size = 121768, upload-time = "2026-07-09T09:02:08.16Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkecs"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/66/f8e4a3b9ca70d3ea79c4d200f928ed9ffdf4910ac01be4864967408c8f18/huaweicloudsdkecs-3.1.204-py3-none-any.whl", hash = "sha256:dc5715d782c0260b901c793d009d5e632257acb04257b6f2c6631e415c589343", size = 765699, upload-time = "2026-07-09T09:02:39.272Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkelb"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/4b/9bdc7e2066419d967e9b812cfacfb9c4996a8e977dc39853309a3c1ac9e2/huaweicloudsdkelb-3.1.204-py3-none-any.whl", hash = "sha256:620247c2b2a7f20e7da8b18fe9c64e29972055f015bc35270fb5b43243dc4830", size = 1292397, upload-time = "2026-07-09T09:02:45.656Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkevs"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/cf/531dc55fd9d0f3bbd3eef24c7e4d78c6a1ba8eb80fa506e3574d72bcc98a/huaweicloudsdkevs-3.1.204-py3-none-any.whl", hash = "sha256:9118ac4c576e54aa7eaa926949e2b6824c5f038a2274b51d9a304d37fc0d7e2f", size = 251404, upload-time = "2026-07-09T09:02:50.05Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkiam"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/9a/7da0fbe9b83bc7a7f6d586366b81e829ed355a57419d2184b7dd51f8c2a3/huaweicloudsdkiam-3.1.204-py3-none-any.whl", hash = "sha256:0021e204f81ceef2640017e517adb72ba56c9ced03f071a0265b10bc9759badf", size = 1251350, upload-time = "2026-07-09T09:03:05.467Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkkms"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/3a/7392617d585cb2005f7d9ade0b0e0e493a7a88daf56e8dc39e4e219cc5d0/huaweicloudsdkkms-3.1.204-py3-none-any.whl", hash = "sha256:378986f33113ce99f445ef318d1c7dda89e361d16c008e5ef9793981d8385376", size = 275690, upload-time = "2026-07-09T09:03:29.833Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkobs"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/49/28a09e1e33d1c039be22ee4171efaa739351653c7aa88d3a2f7a78d90217/huaweicloudsdkobs-3.1.204-py3-none-any.whl", hash = "sha256:8c5830fa30293185964d98e524887fc510c8e17ca2fadb4563dad10910f37b13", size = 235360, upload-time = "2026-07-09T09:03:52.171Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkrds"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/7d/721f162c46e3de604a73674223bf6c6bc6cf7ade25b3751a71288f4dd122/huaweicloudsdkrds-3.1.204-py3-none-any.whl", hash = "sha256:a790b5b3c457a608e5679c101f463b4d037dd9a8a66f6e46144a9e5a4b37780f", size = 1626906, upload-time = "2026-07-09T09:04:06.936Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkvpc"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/b5/4baa27c3a275ea92806068e35e06f249a30add8dd57c777bb45841f63406/huaweicloudsdkvpc-3.1.204-py3-none-any.whl", hash = "sha256:c57d6b6d2f70deca91e86f7956b33fc9ac4991b431f0d608c3632231119f8970", size = 1124332, upload-time = "2026-07-09T09:04:39.797Z" },
+]
+
+[[package]]
+name = "huaweicloudsdkwaf"
+version = "3.1.204"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huaweicloudsdkcore" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/21/01590dce200be487756451f5e9efb99da7810688062d177e4b58d6062465/huaweicloudsdkwaf-3.1.204-py3-none-any.whl", hash = "sha256:b2355276e0029808f45e2d1bd3eb14b37d2da9417e61e642ffe0e2b748ca8283", size = 1337762, upload-time = "2026-07-09T09:04:43.688Z" },
+]
+
[[package]]
name = "humanfriendly"
version = "10.0"
@@ -3920,21 +4086,21 @@ wheels = [
[[package]]
name = "microsoft-kiota-abstractions"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
{ name = "std-uritemplate" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8f/94/37315b82a1bcc08145e5bc2af7396a4be8160ac138ec269611c3b9589b7a/microsoft_kiota_abstractions-1.9.9.tar.gz", hash = "sha256:5df9a8e0517a4568726c2cac6d9789284cc6ffa66043b68eba42ae55749fb861", size = 24468, upload-time = "2026-03-02T21:03:50.133Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/e1/39de28380fc0eddf12f66099469fb7561bc38f577ea06e3a074751ebbcd9/microsoft_kiota_abstractions-1.9.10.tar.gz", hash = "sha256:8eb62d64c35ad0eeb4e8bcdbb143c0b308dc4a494e757f8e44cb959d34f44ecf", size = 24473, upload-time = "2026-03-12T17:27:15.398Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/53/6a/7d5a1a8131f0eccc6b45839c091aa00ba29661854e7defaa7936cf342fa7/microsoft_kiota_abstractions-1.9.9-py3-none-any.whl", hash = "sha256:8d0a14eda42f3f0ccac2e9512227a338f69998dc9b782fd21cb8ca7c48302caa", size = 44453, upload-time = "2026-03-02T21:03:51.11Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/59/bf0cb26c80fbd3fa882df8474ad87e9dbd742656c376388c427c4e314171/microsoft_kiota_abstractions-1.9.10-py3-none-any.whl", hash = "sha256:cd169067ebe48e6feea1258630807034239e0c61c2abe5fd66896a58177e8f05", size = 44462, upload-time = "2026-03-12T17:27:16.532Z" },
]
[[package]]
name = "microsoft-kiota-authentication-azure"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -3943,14 +4109,14 @@ dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ca/ce/5ae8b37ee4a50f0ed5e092c2d0105d60b592e6102a190959f76658a0994c/microsoft_kiota_authentication_azure-1.9.9.tar.gz", hash = "sha256:aca5e7dc8a0a28224f9025a479349ac2f9aaf166bfd6bc707f232658b45eec28", size = 5000, upload-time = "2026-03-02T21:04:02.355Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/53/7760f979c141ec590f0c1cfcb92b3e410eb2909cc19feb42f3fce78db171/microsoft_kiota_authentication_azure-1.9.10.tar.gz", hash = "sha256:b9f10a9fa86e36114abfee448d2dab91a502d6a55d349a306e2e41a1218fe1ad", size = 4999, upload-time = "2026-03-12T17:27:26.323Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/98/de/dc504324b776d00a420886cc6f39e04be2cf48cab0e9b18f8450a5efcc29/microsoft_kiota_authentication_azure-1.9.9-py3-none-any.whl", hash = "sha256:73dc21a1a2861ea78a135327291db3322e2255542a18b311dd03fd908342e902", size = 6951, upload-time = "2026-03-02T21:04:03.18Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/4a/e7852f9358d897ada1eec4e825c815761befe36df4defa79f1ae6c7b588c/microsoft_kiota_authentication_azure-1.9.10-py3-none-any.whl", hash = "sha256:b5d98b0d17173c61c0c7ab4274ea4ca69253b3c13424137758034506694964e9", size = 6961, upload-time = "2026-03-12T17:27:27.238Z" },
]
[[package]]
name = "microsoft-kiota-http"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx", extra = ["http2"] },
@@ -3958,57 +4124,57 @@ dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5d/3f/fc18eb0d1d845daf6355fd54fd990af7f7e10043ef6a6da39b9e5981cbaf/microsoft_kiota_http-1.9.9.tar.gz", hash = "sha256:ae672b145df71b644f8da0951767a12a4ce47a40576d86eba19b7c22d9e160f9", size = 21493, upload-time = "2026-03-02T21:04:11.662Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/e5/20972b620bd8cca086c284e97b285d437c108a23fee122ad7b92bd246c1a/microsoft_kiota_http-1.9.10.tar.gz", hash = "sha256:af1838d091f76426c974897357093ed977ce66f1d808cb161c190de873bb5833", size = 21493, upload-time = "2026-03-12T17:27:35.393Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/6a/cc1b1055b4b6d4dfc1be7a71917c2f0ef19c070c6a18b16d3c1032d20925/microsoft_kiota_http-1.9.9-py3-none-any.whl", hash = "sha256:a5b1b217ac9afeb4054f12515417e3b1d2be12a9385a70a41d18d64379ea2e7e", size = 31945, upload-time = "2026-03-02T21:04:12.328Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/f4/78ce18330a626138b2ff6bb62574adac01e8b9ee87c1349ddfeb9cab0556/microsoft_kiota_http-1.9.10-py3-none-any.whl", hash = "sha256:6127032c8d94f8607e4d36d0822b88bc8689ab368b4c00d6c7beb7d2d0f2ab10", size = 31960, upload-time = "2026-03-12T17:27:36.1Z" },
]
[[package]]
name = "microsoft-kiota-serialization-form"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/b4/18e9fce60a30c8b6ea0a6278fb81cf352127340d48df2d7c52ff1b579488/microsoft_kiota_serialization_form-1.9.9.tar.gz", hash = "sha256:3cdc8b172baec5b5282af72f2ce02715edcd23252ce0b5af96075256edd75114", size = 9015, upload-time = "2026-03-02T21:04:20.39Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/90/7e1a090a2099acae1a1baa9a0762214b73b63d9268369b510994f75f54e4/microsoft_kiota_serialization_form-1.9.10.tar.gz", hash = "sha256:4c6655d8cd479d1ada63fdfe6a272e50d87d7c8369dbc8e13833ba4787fc798b", size = 9012, upload-time = "2026-03-12T17:27:44.214Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/24/eb8436b882f1473bd0a868848d214df3df2d9b3db8e5422d111032f1114f/microsoft_kiota_serialization_form-1.9.9-py3-none-any.whl", hash = "sha256:1c426d4f0d463fc9215c41d7fa0f3dc5fe8d3c80573d555cf63ea67000148d84", size = 10718, upload-time = "2026-03-02T21:04:21.25Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/4c/5092fc896b34c21e8b9c03c63006b313a81e2377176a69c97aa6a9c8f5bb/microsoft_kiota_serialization_form-1.9.10-py3-none-any.whl", hash = "sha256:765d3f6408668f58bfdf892c32b45967c579d9131f3ba5a6b6868cb7ab956bfe", size = 10728, upload-time = "2026-03-12T17:27:45.103Z" },
]
[[package]]
name = "microsoft-kiota-serialization-json"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b8/2f/d36eba916c00136da122d1701acb862c5b1f2e22b6dc6fa4e0f4abda2786/microsoft_kiota_serialization_json-1.9.9.tar.gz", hash = "sha256:9b27479427f49bbac15ead8e8ff0176e47fcdf81153611acc408f5f399342079", size = 9545, upload-time = "2026-03-02T21:04:29.177Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/0e/55afd533a764ba77da988b7ca4242c84867a3a25f2ff0bf4c2b24b5e8fca/microsoft_kiota_serialization_json-1.9.10.tar.gz", hash = "sha256:6063028f30dd67afa2db20a72d9bde5e5d26d468f8bdedadd1445cf7c7630e17", size = 9746, upload-time = "2026-03-12T17:27:53.015Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/7b/b3f606ef2dcbdebe12ae27004ed6e7542370cb2494265f11a8877a1de2d1/microsoft_kiota_serialization_json-1.9.9-py3-none-any.whl", hash = "sha256:bb80b93e81bab41dc142e9b254f79bf0b7b9fe49a796ca0c8e8691925bd3967f", size = 11210, upload-time = "2026-03-02T21:04:29.844Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/56/d14c0185c8092abde1a60ad2bdd4480bb2ddb551ce71c6de1e6133a4d8d1/microsoft_kiota_serialization_json-1.9.10-py3-none-any.whl", hash = "sha256:0545ae910160b19caaa8c30c90c7416e1966294fbd6cc5af01f0e116a18f223a", size = 11452, upload-time = "2026-03-12T17:27:53.909Z" },
]
[[package]]
name = "microsoft-kiota-serialization-multipart"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5f/44/24087f0fac7c5682c13c7fb61468a0c5a5185b9f243de3a99309aa6fcaa7/microsoft_kiota_serialization_multipart-1.9.9.tar.gz", hash = "sha256:f8730be6da5f6c63a6bf4ea310a9723b9998a47a04745887dc156d08f119a829", size = 5162, upload-time = "2026-03-02T21:04:48.1Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/34/eadc15c2a3131e2a76126f3112c32b73502cb5a335e2e40cac2877e5d843/microsoft_kiota_serialization_multipart-1.9.10.tar.gz", hash = "sha256:8f2da4f93e79b09f9738b6889685e47acfafcca870db94ab1d4cd233d69e4268", size = 5167, upload-time = "2026-03-12T17:28:18.507Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/61/db/6b988fdf771c3d07dff4a116176d575832daf2a43823444d145d71da5b61/microsoft_kiota_serialization_multipart-1.9.9-py3-none-any.whl", hash = "sha256:572e9cbafa2eb946452cdadfb019a4e9245768c0d61c3089d3436d4f5106c550", size = 6696, upload-time = "2026-03-02T21:04:48.98Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/40/345cbcee6c52b4261fedf4ae2ff8573aec47ce4ae2015ea8b57c75ef978b/microsoft_kiota_serialization_multipart-1.9.10-py3-none-any.whl", hash = "sha256:7cadc26483b567c738f926b044521569e0b797446053c9e8eab02269d4a81062", size = 6708, upload-time = "2026-03-12T17:28:19.397Z" },
]
[[package]]
name = "microsoft-kiota-serialization-text"
-version = "1.9.9"
+version = "1.9.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-kiota-abstractions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/3c/d244ad08e03003134871698aa54de8243bcc61c0faf3ab114293bb76d6ad/microsoft_kiota_serialization_text-1.9.9.tar.gz", hash = "sha256:18bc0764dda4078a4c953300253344e05d0cdb9c17136f1a2f695d438cedb402", size = 7325, upload-time = "2026-03-02T21:04:37.567Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/a6/28a4a8d5c01f08e363135fc9585cab3c02d1b1a69c3c16032e6abb35dfed/microsoft_kiota_serialization_text-1.9.10.tar.gz", hash = "sha256:cfc433c2a95ea3c3ec43c8b09002fbf65c998c5c0571205df161fe0e9d5d8de7", size = 7326, upload-time = "2026-03-12T17:28:01.621Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/37/f8/43f8d00fed6e090810d3ce0c05e06c23eaa5dee6e87ab1fb89d96ca9559f/microsoft_kiota_serialization_text-1.9.9-py3-none-any.whl", hash = "sha256:84418119d4929a76fde7f31e957e240e003bf145757838b9aa3a0f36dec1b789", size = 8885, upload-time = "2026-03-02T21:04:38.76Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/bf/dd36e4a6d1cff3f2d30f03e2479cd38210e32d4715bb6a9f0e2737f13604/microsoft_kiota_serialization_text-1.9.10-py3-none-any.whl", hash = "sha256:742890cfd4450d12f58d42da7cfa474fe1ee5d6442e016bf70ab76e5c876c0ea", size = 8896, upload-time = "2026-03-12T17:28:02.328Z" },
]
[[package]]
@@ -4260,20 +4426,22 @@ wheels = [
[[package]]
name = "oci"
-version = "2.169.0"
+version = "2.183.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "circuitbreaker" },
+ { name = "crc32c" },
{ name = "cryptography" },
+ { name = "pyjwt", extra = ["crypto"] },
{ name = "pyopenssl" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/11/f4/3c2eddccc75dd06a692dbb3290f20f4bc733d99dc60de21f22d65efdeae4/oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa", size = 16885837, upload-time = "2026-03-31T06:14:58.981Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/2a/77bd6cbf1c69b2f368fe3d6462d84369b0cba15e37ce713cdc08d459b95a/oci-2.183.0.tar.gz", hash = "sha256:ff572ef5f2030a788796bb509d257e6a41c6510ef9b4b6a75a079efd06e533ce", size = 17759723, upload-time = "2026-07-28T06:02:29.76Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/bf/19643bd939ab595193779ee25c2c12aef8e9a54e0a68de5ed79f209702e3/oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b", size = 34460447, upload-time = "2026-03-31T06:14:51.373Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/de/8574b3e527996a099d196e87794a4652d91a0c3185fcc7fdbb5649b75a8a/oci-2.183.0-py3-none-any.whl", hash = "sha256:bd789c98a94d7c5ea08c20d11dcf68c9cd1ad479b134727d80a930b84387070b", size = 36133501, upload-time = "2026-07-28T06:02:18.239Z" },
]
[[package]]
@@ -4467,39 +4635,33 @@ wheels = [
[[package]]
name = "pillow"
-version = "12.2.0"
+version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
- { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
- { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
- { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
- { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
- { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
- { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
- { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
- { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
- { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
- { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
- { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
- { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
- { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
- { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
- { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
- { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
- { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
- { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
- { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
- { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
- { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
- { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
- { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
- { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
- { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
- { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
- { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
+ { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
+ { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
+ { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
+ { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
+ { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
+ { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
+ { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
+ { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
@@ -4673,8 +4835,8 @@ wheels = [
[[package]]
name = "prowler"
-version = "5.32.0"
-source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#5dac8a0a53272e4db68c476fb969dc03e88beb68" }
+version = "5.38.0"
+source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#b3d174d0c1eb202ed7cb9a9daf0500683f4443be" }
dependencies = [
{ name = "alibabacloud-actiontrail20200706" },
{ name = "alibabacloud-credentials" },
@@ -4729,6 +4891,17 @@ dependencies = [
{ name = "google-api-python-client" },
{ name = "google-auth-httplib2" },
{ name = "h2" },
+ { name = "huaweicloudsdkcore" },
+ { name = "huaweicloudsdkcts" },
+ { name = "huaweicloudsdkecs" },
+ { name = "huaweicloudsdkelb" },
+ { name = "huaweicloudsdkevs" },
+ { name = "huaweicloudsdkiam" },
+ { name = "huaweicloudsdkkms" },
+ { name = "huaweicloudsdkobs" },
+ { name = "huaweicloudsdkrds" },
+ { name = "huaweicloudsdkvpc" },
+ { name = "huaweicloudsdkwaf" },
{ name = "jsonschema" },
{ name = "kingfisher-bin" },
{ name = "kubernetes" },
@@ -4762,7 +4935,7 @@ dependencies = [
[[package]]
name = "prowler-api"
-version = "1.35.0"
+version = "1.40.0"
source = { virtual = "." }
dependencies = [
{ name = "cartography" },
@@ -4978,25 +5151,24 @@ wheels = [
[[package]]
name = "py-ocsf-models"
-version = "0.8.1"
+version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cryptography" },
{ name = "email-validator" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/70/61e2f9ce3d7e83aa5339ed6ae17e473c15c7a36f161c6dbea0e939e3af0c/py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c", size = 36540, upload-time = "2026-02-12T16:50:15.233Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/70/d6/f0787cbe953e3cf6ef4430f3cc7d66cbbaabe4b20cb82cc27cc2d21e622a/py_ocsf_models-0.10.0.tar.gz", hash = "sha256:29abaa5a3d4ebba0e2a21757508a4848fa5e1d57da233af57e580f97f0223c59", size = 36498, upload-time = "2026-07-13T07:05:44.448Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/18/63790884bf33f820e2c60f8d5038b5d6de967a03343ddf237c054e1d6d08/py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e", size = 64354, upload-time = "2026-02-12T16:50:12.983Z" },
+ { url = "https://files.pythonhosted.org/packages/75/56/eca45ec87a02f930cc7eaa7cb36660f69fb00c3d77bb4a84bb92d6c94c25/py_ocsf_models-0.10.0-py3-none-any.whl", hash = "sha256:a9d1e245b1c9fba1d2cb8c042253ef1b83a2dbfec30ed69975bbce599b4510bb", size = 64334, upload-time = "2026-07-13T07:05:42.93Z" },
]
[[package]]
name = "pyasn1"
-version = "0.6.3"
+version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
@@ -5187,6 +5359,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/ff/7f52c1461d8ceaefa989d2700a027f84427879bb7571145bbffdec5d5f4a/pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6", size = 519603, upload-time = "2024-06-28T13:10:23.526Z" },
]
+[[package]]
+name = "pymongo"
+version = "4.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" },
+ { url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" },
+ { url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" },
+ { url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" },
+ { url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" },
+ { url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" },
+ { url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" },
+ { url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" },
+ { url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" },
+]
+
[[package]]
name = "pymsalruntime"
version = "0.18.1"
@@ -5223,15 +5426,15 @@ wheels = [
[[package]]
name = "pyopenssl"
-version = "26.0.0"
+version = "26.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" },
+ { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
]
[[package]]
@@ -5556,6 +5759,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
]
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
[[package]]
name = "requestsexceptions"
version = "1.4.0"
@@ -5774,6 +5989,37 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/06/c6dcc975a1e7d89bc764fd271da8138b318e18080b48e7f1acd2ab63df28/shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6", size = 57939, upload-time = "2023-12-17T01:42:02.426Z" }
+[[package]]
+name = "simplejson"
+version = "4.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0e/2a/54837395a3487c725669428d513293612a48d82b95a0642c936932e5d898/simplejson-4.1.1.tar.gz", hash = "sha256:c08eb9f7a90f77ae470e19a07472e9a79ebc0d1c2315d86a72767665bd5ba79f", size = 118860, upload-time = "2026-04-24T19:24:59.819Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/25/39013ffe279d90093ec1c848565b3683c586906c10fa55d9000ec29d046b/simplejson-4.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2867c64d92abd1992c15666fae198203093f593e43d6b81adf176bae530d493a", size = 111538, upload-time = "2026-04-24T19:22:49.051Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/ae/2c272971c8a87e2539c54a98eb6ff037bee1e2e93943c3986cf7500a4f3a/simplejson-4.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c47c46e16c8ea9e4850061e6ed5aa2b9cd2074cb2274bfd9c138cba15ce7453", size = 90594, upload-time = "2026-04-24T19:22:50.408Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/a2/6eebfb99dedc139f549200f61ade6d1890ac5707c5d427bdfa6fe39c9313/simplejson-4.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e294e33dbf316a9bbdd4030d46503c9b0f19470ae7ad6af5bae6c426bc2e869f", size = 90718, upload-time = "2026-04-24T19:22:51.694Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7e/c9e6c0c4ad8415e64dad0c47f619b556b02680a41631b4dbc281d55dc54d/simplejson-4.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ce252b28fddbdd83db5bd7d93dad2a8a591d7ada098afec9c1b23d6b722a7a4", size = 180901, upload-time = "2026-04-24T19:22:53.025Z" },
+ { url = "https://files.pythonhosted.org/packages/34/09/69e331e3994b1ed9be6ce9ace4ade704e7ed503edf869929ca7bb404eda8/simplejson-4.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c44ef6b02a4eb67ed17a72342341792149b3ff46f15426c26e970e49addf327", size = 178133, upload-time = "2026-04-24T19:22:54.574Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/40/ed806f24afef295c1032448f5ff6f6f2979392d5645ddb9f4fed7f38194d/simplejson-4.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82bfca2b85a34178c25829c703f0a9e9f113a5af7539285bd3efb583a0bf1ba3", size = 188155, upload-time = "2026-04-24T19:22:56.044Z" },
+ { url = "https://files.pythonhosted.org/packages/38/94/8d6f515b827b0f7881a49c8c1ac6920b7ae9428939ef04238c973278b42a/simplejson-4.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e4b23f71dd781f8830f1663dc01a4944d3dbf87a1f93d78fba1cf64722d0ccf", size = 176225, upload-time = "2026-04-24T19:22:57.981Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/fd/6dffb4956563d48bbe46b91ff341adae34920e94008fd6b8d728072abfc7/simplejson-4.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:82fee635d7b73ad801030b05a75fbd34a098da0c2ecf600667a03636d09e1e42", size = 185535, upload-time = "2026-04-24T19:22:59.618Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d2/a509ee37763e79aec75d68f8521db1440306edeba3b8b4064ab4ee8bf1d9/simplejson-4.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68e62eda21192c5ea9bb92d571ca46a4477fef48762f50d433de2b4253051551", size = 179302, upload-time = "2026-04-24T19:23:01.324Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/23/5b343bfd2a79d3b6818e4db3586c405a001a090d4c89d336e31273ce7177/simplejson-4.1.1-cp311-cp311-win32.whl", hash = "sha256:ffd3d82294b47f5ec64050021ace95fd62628a0c1cc8bbf4d06d2d1fb697e055", size = 88408, upload-time = "2026-04-24T19:23:02.808Z" },
+ { url = "https://files.pythonhosted.org/packages/38/04/df9b37aedbd524dca20840d25ebe01d6ae486b89792aeff5d15b9c4114f7/simplejson-4.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:78a3fe0995be42bed62a26aa78e0e0b4d87c6545785346b9cc898f3389569a35", size = 90526, upload-time = "2026-04-24T19:23:04.408Z" },
+ { url = "https://files.pythonhosted.org/packages/60/25/e90998fe8e480eb43b966c09e835379887d427567ebd496563d3b1e16b19/simplejson-4.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:19040a17154dc03d289bab68d73ce0a6a0be01de30c584bbdd93490bead14b22", size = 112414, upload-time = "2026-04-24T19:23:06.084Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/a0/abd4785f36c3400f1fbb21f517be39295a750a714f04b7ee175adf6ef580/simplejson-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a94ebaecdbaa80d9551a3ec6bf0c9302fc8b53ab6c1b2bfd498a1df4cb28158d", size = 91120, upload-time = "2026-04-24T19:23:07.877Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/78/fc060d2e3b13c6ec59288574b8efac64075e316b2afba4396a56b2422f78/simplejson-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67341c95c0a168ab4a6d1e807e50463f1c8da932c3286d81e201266c427061fa", size = 91055, upload-time = "2026-04-24T19:23:09.264Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b6/156a8de1e1b47694f0e7de6675866936608d45dc68388fd017d36f8693be/simplejson-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45ec18e337fec538b7e902d489505c450b2454653d1290f3f50385e6fd8aa607", size = 190297, upload-time = "2026-04-24T19:23:11.226Z" },
+ { url = "https://files.pythonhosted.org/packages/86/1c/e4d0eab695be3eb21d0f46bce820752031f03e7113f9c80a9b3c73ee7157/simplejson-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:820c69a4710400e9b248d5670647d60be58824369282d3925e516b3ff1a7cd82", size = 187002, upload-time = "2026-04-24T19:23:12.982Z" },
+ { url = "https://files.pythonhosted.org/packages/76/0e/7f5a59d29426b062d5928fb88b403c3f797129d53be7102f955dbe51aa44/simplejson-4.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e708d373a10e4378ef2d59f8361850c7150fd907ed49efe49bc5492160476d1", size = 195146, upload-time = "2026-04-24T19:23:14.517Z" },
+ { url = "https://files.pythonhosted.org/packages/78/18/9943db224dd4d5fa3c090c3e56a94c37b254338c83995ec5680285111c40/simplejson-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:980fc33353f81fd12d8c49d44f8c2760d1dc8192285e627c5180d141035b228a", size = 183931, upload-time = "2026-04-24T19:23:16.742Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/08/9a690da9a766161c06c627d805362cf159f1abe480969372b2897649b955/simplejson-4.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de2ed102fff88dacf543699f53ee3a533cc11539a39baa176b7e09dd783069d6", size = 192228, upload-time = "2026-04-24T19:23:18.33Z" },
+ { url = "https://files.pythonhosted.org/packages/05/88/bd8aad36b451ffb0e0a3f721d695a88befa6d1ac7d1e02ae788ca7ff4029/simplejson-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785ff8edc0e28bf773a32543a6bbed46351453c997b3f6709c744e3c2f7eabb", size = 187808, upload-time = "2026-04-24T19:23:21.165Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ee/14f91db0d1f481533b651dafbf8cd0da088d9817f7af30c68f7f19f9c847/simplejson-4.1.1-cp312-cp312-win32.whl", hash = "sha256:2e0d5ead6d14610467ec356ec1f6b5d8a56aa216abaad8d41c8b873b16cf313f", size = 88512, upload-time = "2026-04-24T19:23:22.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c4/90de06b2d8737c68c05ff9274113f854dbf6a5f28b7a955212111672cb57/simplejson-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63a5451f557d6be48a231bae932458655c620902b868170b2f1c8afed496f6b4", size = 90748, upload-time = "2026-04-24T19:23:24.494Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/6a/8b74c52ffd33dbbde00fe7251fee6a0acdc8cea33f7a43805aed258fb79b/simplejson-4.1.1-py3-none-any.whl", hash = "sha256:2ce92b3748f02423e26d2bfb636fb9d7a8f67c8f5854dcae69d350d123b2eee2", size = 69195, upload-time = "2026-04-24T19:24:57.962Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -6203,16 +6449,16 @@ wheels = [
[[package]]
name = "workos"
-version = "6.0.8"
+version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "httpx" },
{ name = "pyjwt", extra = ["crypto"] },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ca/0d/0a7f78912657f99412c788932ea1f3f4089916e77bdef7d2463842febe08/workos-6.0.8.tar.gz", hash = "sha256:43aa3f1992a0a4ca8933d9b6e5ada846dd3b1fe0ee10e64c876ee2000fc6090d", size = 178137, upload-time = "2026-04-24T18:48:03.203Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/f6/bb27fe77e70b5e2c5da72500ca0ece8b0e8318010fec92c31d68483314e3/workos-8.3.0.tar.gz", hash = "sha256:07b66c2fb287adb593e4d77a2e6cb05b48bd8ff0b2722f343d18eeb5e14f7472", size = 201587, upload-time = "2026-06-30T15:19:22.834Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/3f/3d96da80d650b2f97d58af626053354584f619dbb769051e118bd9cd1ca5/workos-6.0.8-py3-none-any.whl", hash = "sha256:a00dd4930333aded2babbba824f8032eea05c5ca8c44d04a3fa068cf6be6e21a", size = 524505, upload-time = "2026-04-24T18:48:01.389Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ed/7e6fe07c5bc0222fd92c1cf1f3c4c24293e4e5fc7bb5d7df90e4ee61c17f/workos-8.3.0-py3-none-any.whl", hash = "sha256:d0fa842b93bfc5fb33bf49e69cf8c379936cf54b87c6e2f50bcc6dd2e84f8fe4", size = 592275, upload-time = "2026-06-30T15:19:21.333Z" },
]
[[package]]
diff --git a/claude_plugins/prowler/.claude-plugin/plugin.json b/claude_plugins/prowler/.claude-plugin/plugin.json
index 7bf822e2e7..c77187c3b0 100644
--- a/claude_plugins/prowler/.claude-plugin/plugin.json
+++ b/claude_plugins/prowler/.claude-plugin/plugin.json
@@ -22,7 +22,7 @@
"api_key": {
"type": "string",
"title": "Prowler API key",
- "description": "API key token used to authenticate with Prowler Cloud / Prowler App via the Prowler MCP server. Create one at https://cloud.prowler.com.",
+ "description": "API key token used to authenticate with Prowler (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server) via the Prowler MCP server. Create one at https://cloud.prowler.com.",
"sensitive": true,
"required": true
}
diff --git a/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md b/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md
index 1af29f82b9..f8a9ded0c6 100644
--- a/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md
+++ b/claude_plugins/prowler/skills/framework-compliance-triage/SKILL.md
@@ -38,12 +38,12 @@ If the framework is not supported, tell the user, suggest they request it or con
### 1.1 Connect to Prowler Cloud
-Verify the Prowler MCP connection by calling `prowler_app_search_providers` — a successful response returns the list of providers. If the call fails, walk the user through troubleshooting: internet connectivity, Prowler Cloud credentials, and permissions on the Prowler Cloud account.
+Verify the Prowler MCP connection by calling `prowler_search_providers` — a successful response returns the list of providers. If the call fails, walk the user through troubleshooting: internet connectivity, Prowler Cloud credentials, and permissions on the Prowler Cloud account.
For getting accurate information about configurations use `prowler_docs_search` to pull relevant instructions from the Prowler documentation.
### 1.2 Verify the provider is configured (or configure it)
-Call `prowler_app_search_providers` to check whether the target provider (AWS account, Azure Subscription, GitHub Account...) exists in the user's Prowler Cloud account. Handle the result based on what's found:
+Call `prowler_search_providers` to check whether the target provider (AWS account, Azure Subscription, GitHub Account...) exists in the user's Prowler Cloud account. Handle the result based on what's found:
- **Provider not present.** Guide the user through adding and configuring it. Retrieve the relevant connection, credential, and permission instructions with `prowler_docs_search`.
- **Provider present but misconfigured** (missing credentials, insufficient permissions, etc.). Walk the user through fixing the configuration, pulling the relevant guidance with `prowler_docs_search`.
@@ -57,15 +57,15 @@ Call `prowler_app_search_providers` to check whether the target provider (AWS ac
The flow needs at least one completed scan with a compliance report available.
-Look for a completed scan first: call `prowler_app_list_scans` with the selected `provider_id` and `state: ["completed"]`, then call `prowler_app_get_compliance_overview` with each `scan_id` to find one whose compliance report is available. If one is found, continue to the next section.
+Look for a completed scan first: call `prowler_list_scans` with the selected `provider_id` and `state: ["completed"]`, then call `prowler_get_compliance_overview` with each `scan_id` to find one whose compliance report is available. If one is found, continue to the next section.
-If no completed scan has a report, call `prowler_app_list_scans` again with `state: ["available", "executing"]` to detect a scan in progress.
+If no completed scan has a report, call `prowler_list_scans` again with `state: ["available", "executing"]` to detect a scan in progress.
> **Checkpoint — Scan-in-progress decision** *(conditional: an in-progress scan was detected)*
>
> Tell the user a scan is already running and ask whether to wait for it to complete or start a fresh one. Wait for the answer.
-If no scan is running (or the user chose to start a fresh one), trigger a new scan with `prowler_app_trigger_scan` and the `provider_id`. The link `https://cloud.prowler.com/scans?filter%5Bprovider_uid__in%5D={provider_id}` lets the user monitor progress.
+If no scan is running (or the user chose to start a fresh one), trigger a new scan with `prowler_trigger_scan` and the `provider_id`. The link `https://cloud.prowler.com/scans?filter%5Bprovider_uid__in%5D={provider_id}` lets the user monitor progress.
When a scan is in progress (either pre-existing and elected to wait, or just triggered), stop the flow and ask the user to return when it's completed — restart this section to re-check the results.
@@ -85,7 +85,7 @@ Status taxonomy for failed requirements and their findings:
### Report template
-A fresh report is rendered like this (substituting values from the `prowler_app_get_compliance_framework_state_details` Prowler MCP tool response):
+A fresh report is rendered like this (substituting values from the `prowler_get_compliance_framework_state_details` Prowler MCP tool response):
````markdown
# Compliance report:
@@ -120,7 +120,7 @@ A fresh report is rendered like this (substituting values from the `prowler_app_
Resolve the report path for the current `compliance_id` and provider account.
-If the file does not exist, call `prowler_app_get_compliance_framework_state_details` for the target scan, render the template above, and write the file with one initialization entry in the activity log.
+If the file does not exist, call `prowler_get_compliance_framework_state_details` for the target scan, render the template above, and write the file with one initialization entry in the activity log.
If the file exists, read it and compare its `Scan ID` to the target scan from section 1.3. When the scan matches, reuse the file and summarize remaining `[FAIL]` and `[IN PROGRESS]` items in chat.
@@ -128,7 +128,7 @@ If the file exists, read it and compare its `Scan ID` to the target scan from se
>
> Tell the user the report on disk was generated from a different scan and ask whether to refresh it from the new scan. Wait for the answer.
-On confirmation, regenerate the failed-requirements section from the new `prowler_app_get_compliance_framework_state_details` response, carry forward the **Global remediation approach** block and the full activity log, and append an activity-log entry noting the scan change.
+On confirmation, regenerate the failed-requirements section from the new `prowler_get_compliance_framework_state_details` response, carry forward the **Global remediation approach** block and the full activity log, and append an activity-log entry noting the scan change.
Once the file is current, surface the top failing requirements in chat: sort by finding count descending, show the top 5 with their codes and counts, and point to the file path for the full list.
@@ -174,7 +174,7 @@ Once approved, the loop proceeds through the batch without further prompts unles
Pick the first `[FAIL]` requirement at the top of the failed-requirements section. Move its status and every finding under it to `[IN PROGRESS]`, and add a `**Fix plan**:` sub-bullet describing what will be done.
-Call `prowler_app_get_finding_details` for each `finding_id` to retrieve the failing resource and the Prowler Hub's remediation guidance for that check using the tool `prowler_hub_get_check_details` with the `check_id` from the finding details. Summarize the guidance in chat, and append it to the `**Fix plan**` note for each finding.
+Call `prowler_get_finding_details` for each `finding_id` to retrieve the failing resource and the Prowler Hub's remediation guidance for that check using the tool `prowler_hub_get_check_details` with the `check_id` from the finding details. Summarize the guidance in chat, and append it to the `**Fix plan**` note for each finding.
If a finding does not apply to the target resource (Organization-only check on a User account, paid-tier feature, missing resource type, etc.), set the requirement status to `[SKIPPED]` with the reason, log it in the activity log, and move on without attempting the fix — even if it was missed during §3.2.
@@ -194,6 +194,6 @@ Move to the next `[FAIL]` requirement and repeat from section 3.3.
> **Checkpoint — Rescan trigger** *(conditional: no `[FAIL]` requirements remain; all are `[FIXED-UNVERIFIED]` or `[SKIPPED]`)*
>
-> Summarize what was applied, list any `[SKIPPED]` items with reasons, and ask whether to trigger a fresh scan with `prowler_app_trigger_scan` to verify the fixes end-to-end. Wait for the answer.
+> Summarize what was applied, list any `[SKIPPED]` items with reasons, and ask whether to trigger a fresh scan with `prowler_trigger_scan` to verify the fixes end-to-end. Wait for the answer.
On confirmation, trigger the rescan. When it completes, restart section 2.1 with the carry-forward path — requirements no longer in the new FAIL list move to `[PASS]`, anything still failing reverts to `[FAIL]` with the previous fix attempt visible in the activity log.
diff --git a/codecov.yml b/codecov.yml
index ca31ca8dd4..21398b70f7 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -6,6 +6,19 @@ component_management:
- component_id: "api"
paths:
- "api/**"
+ - component_id: "mcp_server"
+ paths:
+ - "mcp_server/**"
+
+flags:
+ api:
+ paths:
+ - "api/**"
+ carryforward: true
+ mcp:
+ paths:
+ - "mcp_server/**"
+ carryforward: true
comment:
layout: "header, diff, flags, components"
diff --git a/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml b/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml
index 8e219a9271..a76982d403 100644
--- a/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml
+++ b/contrib/k8s/helm/prowler-app/templates/api/configmap.yaml
@@ -7,4 +7,4 @@ metadata:
data:
{{- range $key, $value := .Values.api.djangoConfig }}
{{ $key }}: {{ $value | quote }}
- {{- end }}
\ No newline at end of file
+ {{- end }}
diff --git a/contrib/k8s/helm/prowler-app/templates/api/role.yaml b/contrib/k8s/helm/prowler-app/templates/api/role.yaml
index 172b035076..f55d3c7dc9 100644
--- a/contrib/k8s/helm/prowler-app/templates/api/role.yaml
+++ b/contrib/k8s/helm/prowler-app/templates/api/role.yaml
@@ -26,4 +26,4 @@ roleRef:
subjects:
- kind: ServiceAccount
name: {{ include "prowler.api.serviceAccountName" . }}
- namespace: {{ .Release.Namespace }}
\ No newline at end of file
+ namespace: {{ .Release.Namespace }}
diff --git a/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml b/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml
index 98ae3ae9d5..32408cb0ec 100644
--- a/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml
+++ b/contrib/k8s/helm/prowler-app/templates/worker/scaled-object.yaml
@@ -18,15 +18,12 @@ spec:
triggers:
- type: {{ .Values.worker.keda.triggerType }}
metadata:
- userName: "postgres"
- passwordFromEnv: POSTGRES_ADMIN_PASSWORD
- host: {{ .Release.Name }}-postgresql
- port: {{ .Values.postgresql.port | quote }}
- dbName: {{ .Values.postgresql.auth.database | quote }}
- sslmode: disable
- # Query for KEDA to count the number of scans that are in executing, available, or scheduled states,
- # where the scheduled time is within the last 2 hours and is before NOW(). Used for scaling workers.
- query: >-
- SELECT COUNT(*) FROM scans WHERE ((state='executing' OR state='available' OR state='scheduled') and scheduled_at < NOW() and scheduled_at > NOW() - INTERVAL '2 hours')
- targetQueryValue: "1"
+ userName: {{ .Values.worker.keda.postgresql.userName | quote }}
+ passwordFromEnv: {{ .Values.worker.keda.postgresql.passwordFromEnv | quote }}
+ host: {{ .Values.worker.keda.postgresql.host | default (printf "%s-postgresql.%s.svc.cluster.local" .Release.Name .Release.Namespace) | quote }}
+ port: {{ .Values.worker.keda.postgresql.port | quote }}
+ dbName: {{ .Values.worker.keda.postgresql.database | default .Values.postgresql.auth.database | quote }}
+ sslmode: {{ .Values.worker.keda.postgresql.sslmode | quote }}
+ query: {{ .Values.worker.keda.query | quote }}
+ targetQueryValue: {{ .Values.worker.keda.targetQueryValue | quote }}
{{- end }}
diff --git a/contrib/k8s/helm/prowler-app/values.yaml b/contrib/k8s/helm/prowler-app/values.yaml
index ed390af29e..a5607575bc 100644
--- a/contrib/k8s/helm/prowler-app/values.yaml
+++ b/contrib/k8s/helm/prowler-app/values.yaml
@@ -189,6 +189,11 @@ api:
DJANGO_STALE_WHILE_REVALIDATE: "60"
DJANGO_MANAGE_DB_PARTITIONS: "True"
DJANGO_BROKER_VISIBILITY_TIMEOUT: "86400"
+ # Caps the Celery prefork pool size on the worker pods. Without it, Celery
+ # sizes the pool from the number of visible CPUs, so on large nodes the
+ # worker spawns one child per CPU, each loading the full Prowler SDK, and
+ # OOMKills under memory pressure. Raise it on bigger workers.
+ DJANGO_CELERY_WORKER_CONCURRENCY: "2"
# Secret names to be used as env vars for api, worker, and worker_beat.
secrets: []
@@ -422,10 +427,61 @@ worker:
pollingInterval: 30
# -- The cooldown period in seconds for scaling
cooldownPeriod: 120
- # -- The trigger type for scaling (cpu or memory)
+ # -- The KEDA scaler type. Only `postgresql` is supported by the default query below.
triggerType: "postgresql"
- # -- The target utilization percentage for the worker pods
- value: "50"
+ # PostgreSQL connection used by the scaler query. The KEDA operator opens this
+ # connection from its own namespace, so `host` must resolve from there. The
+ # defaults target the bundled postgresql subchart; set them explicitly when
+ # using an external database (postgresql.enabled: false).
+ postgresql:
+ # -- Scaler database host. Defaults to the bundled "-postgresql..svc.cluster.local" service.
+ host: ""
+ # -- Scaler database port.
+ port: "5432"
+ # -- Scaler database name. Defaults to `postgresql.auth.database`.
+ database: ""
+ # -- User the scaler authenticates as.
+ userName: "postgres"
+ # -- Name of an env var on the worker container holding the password.
+ passwordFromEnv: "POSTGRES_ADMIN_PASSWORD"
+ # -- sslmode for the scaler connection.
+ sslmode: "disable"
+ # -- The scaler divides the query result by this value to get the desired replica count.
+ targetQueryValue: "1"
+ # -- Query the scaler runs to measure pending work. It replaces the previous
+ # 2-hour scheduled-only window, which missed manual scans, older backlogs and
+ # in-progress scans. Override to tune scaling for your workload.
+ #
+ # The default sums three signals:
+ # 1. Scans executing or available, bounded to rows updated in the last 24h so
+ # orphaned rows do not pin the worker up, plus scheduled scans that are due
+ # (no lower bound, so an overdue backlog still scales up).
+ # 2. Scan tasks published in the last 48h that no worker has finished. A PENDING
+ # TaskResult is written at publish time (before_task_publish in api/signals.py),
+ # so Beat's daily publishes are visible even with zero workers. Signal 1 alone
+ # deadlocks with minReplicas 0: every scan row after the first is created by
+ # the worker, so once the initial row ages out of the 24h bound there is
+ # nothing to count and nothing to create more.
+ # 3. Non-scan tasks pending in the last hour. Provider connection checks,
+ # deletions, reports and backfills never touch the scans table, so without
+ # this they are never picked up while the worker is scaled to zero.
+ # This includes reconcile-orphan-tasks, a Beat watchdog that runs every two
+ # minutes, so with minReplicas 0 the worker is woken about that often. Add
+ # it to the excluded task names below, or raise cooldownPeriod, if you would
+ # rather trade watchdog latency for longer idle periods.
+ query: >-
+ SELECT
+ (SELECT COUNT(*) FROM scans
+ WHERE (state IN ('executing', 'available') AND updated_at > NOW() - INTERVAL '24 hours')
+ OR (state = 'scheduled' AND scheduled_at < NOW()))
+ + (SELECT COUNT(*) FROM django_celery_results_taskresult
+ WHERE task_name IN ('scan-perform', 'scan-perform-scheduled')
+ AND status IN ('PENDING', 'RECEIVED', 'STARTED')
+ AND date_created > NOW() - INTERVAL '48 hours')
+ + (SELECT COUNT(*) FROM django_celery_results_taskresult
+ WHERE task_name NOT IN ('scan-perform', 'scan-perform-scheduled')
+ AND status IN ('PENDING', 'RECEIVED', 'STARTED')
+ AND date_created > NOW() - INTERVAL '1 hour')
worker_beat:
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
diff --git a/dashboard/__main__.py b/dashboard/__main__.py
index 664ce3bfa5..6a36bda7ed 100644
--- a/dashboard/__main__.py
+++ b/dashboard/__main__.py
@@ -20,7 +20,7 @@ print_banner()
print(
f"{Fore.GREEN}Loading all CSV files from the folder {folder_path_overview} ...\n{Style.RESET_ALL}"
)
-cli.show_server_banner = lambda *x: click.echo(
+cli.show_server_banner = lambda *_: click.echo(
f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler Cloud{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler CLI{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3:///output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files."
)
@@ -33,148 +33,187 @@ dashboard = dash.Dash(
title="Prowler Dashboard",
)
-# Logo
-prowler_logo = html.Img(
- src="https://cdn.prod.website-files.com/68c4ec3f9fb7b154fbcb6e36/68ffb46d40ed7faa37a592a5_prowler-logo.png",
- alt="Prowler Logo",
+# ``use_pages`` above already imported dashboard/pages/cloud.py and registered
+# every /cloud/* route. Import its metadata now (after app instantiation) so
+# the sidebar and the gated pages share a single source of truth.
+from dashboard.pages.cloud import CLOUD_FEATURES_BY_SLUG # noqa: E402
+
+ICON_DIR = "/assets/images/icons/cloud"
+
+# Official marketing "PROWLER / LOCAL DASHBOARD" lockup (white wordmark + teal
+# gradient sublabel) shown in the expanded sidebar. Vector SVG so it stays crisp
+# at any DPI. The sublabel is right-anchored (text-anchor="end"), so a font
+# fallback widens it leftward rather than clipping at the edge.
+prowler_lockup = html.Img(
+ src=f"{ICON_DIR}/prowler-lockup.svg",
+ alt="Prowler Local Dashboard",
+ className="pc-brand-lockup",
)
-menu_icons = {
- "overview": "/assets/images/icons/overview.svg",
- "compliance": "/assets/images/icons/compliance.svg",
-}
+# Compact brand mark shown only when the sidebar collapses to its icon rail.
+prowler_mark = html.Img(
+ src=f"{ICON_DIR}/prowler-mark.svg",
+ alt="Prowler",
+ className="pc-brand-mark",
+)
+
+# Locally functional destinations (Overview + Compliance).
+DASHBOARD_ITEMS = [
+ {"label": "Overview", "route": "/", "icon": f"{ICON_DIR}/overview.svg"},
+ {
+ "label": "Compliance",
+ "route": "/compliance",
+ "icon": f"{ICON_DIR}/compliance.svg",
+ },
+]
+
+# Gated navigation groups reference the shared feature metadata by slug so the
+# sidebar and the informational pages never drift apart.
+GATED_GROUPS = [
+ ("Upgrade to Prowler Cloud", ["lighthouse-ai", "attack-paths", "findings"]),
+ ("Configuration", ["alerts", "mutelist", "integrations"]),
+ ("Workspace", ["organization"]),
+]
+
+HELP_LINKS = [
+ {
+ "title": "Help",
+ "url": "https://github.com/prowler-cloud/prowler/issues",
+ "icon": f"{ICON_DIR}/help.svg",
+ },
+ {
+ "title": "Docs",
+ "url": "https://docs.prowler.com",
+ "icon": f"{ICON_DIR}/docs.svg",
+ },
+]
-# Function to generate navigation links
-def generate_nav_links(current_path):
- nav_links = []
- for page in dash.page_registry.values():
- # Gets the icon URL based on the page name
- icon_url = menu_icons.get(page["name"].lower())
- is_active = (
- " bg-prowler-stone-950 border-r-4 border-solid border-prowler-lime"
- if current_path == page["relative_path"]
- else ""
- )
- link_class = f"block hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime{is_active}"
+def _mask_style(icon_url):
+ """Inline style rendering a recolorable mask icon from a local asset."""
+ return {
+ "WebkitMaskImage": f"url({icon_url})",
+ "maskImage": f"url({icon_url})",
+ }
- link_content = html.Span(
+
+def _nav_icon(icon_url):
+ return html.Span(className="pc-ico", style=_mask_style(icon_url))
+
+
+def _nav_item(label, route, icon_url, current_path, gated=False):
+ is_active = current_path == route
+ class_name = "pc-nav-item pc-active" if is_active else "pc-nav-item"
+
+ content = [
+ _nav_icon(icon_url),
+ html.Span(label, className="pc-nav-label"),
+ ]
+ if gated:
+ content.append(html.Span("Prowler Cloud", className="pc-pill"))
+
+ return dcc.Link(content, href=route, className=class_name)
+
+
+def _section_label(title):
+ return html.Div(title, className="pc-section")
+
+
+def generate_sidebar(current_path):
+ children = [
+ # Brand lockup: full wordmark when expanded, compact mark when collapsed.
+ html.Div(
+ [prowler_lockup, prowler_mark],
+ className="pc-brand",
+ ),
+ # Dashboards section — the only locally functional destinations.
+ _section_label("Dashboards"),
+ html.Nav(
[
- html.Img(src=icon_url, className="w-5"),
- html.Span(
- page["name"], className="font-medium text-base leading-6 text-white"
- ),
+ _nav_item(item["label"], item["route"], item["icon"], current_path)
+ for item in DASHBOARD_ITEMS
],
- className="flex justify-center lg:justify-normal items-center gap-x-3 py-2 px-3",
- )
-
- nav_link = html.Li(
- dcc.Link(link_content, href=page["relative_path"], className=link_class)
- )
- nav_links.append(nav_link)
- return nav_links
-
-
-def generate_help_menu():
- help_links = [
- {
- "title": "Help",
- "url": "https://github.com/prowler-cloud/prowler/issues",
- "icon": "/assets/images/icons/help.png",
- },
- {
- "title": "Docs",
- "url": "https://docs.prowler.com",
- "icon": "/assets/images/icons/docs.png",
- },
+ className="pc-nav",
+ ),
]
- link_class = "block hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime"
-
- menu_items = []
- for link in help_links:
- menu_item = html.Li(
- html.A(
- html.Span(
- [
- html.Img(src=link["icon"], className="w-5"),
- html.Span(
- link["title"],
- className="font-medium text-base leading-6 text-white",
- ),
- ],
- className="flex items-center gap-x-3 py-2 px-3",
- ),
- href=link["url"],
- target="_blank",
- className=link_class,
+ # Gated groups (Prowler Cloud only).
+ for section_title, slugs in GATED_GROUPS:
+ children.append(_section_label(section_title))
+ children.append(
+ html.Nav(
+ [
+ _nav_item(
+ CLOUD_FEATURES_BY_SLUG[slug]["nav_label"],
+ CLOUD_FEATURES_BY_SLUG[slug]["route"],
+ CLOUD_FEATURES_BY_SLUG[slug]["icon"],
+ current_path,
+ gated=True,
+ )
+ for slug in slugs
+ ],
+ className="pc-nav",
)
)
- menu_items.append(menu_item)
- return menu_items
+ # Help and Docs pinned to the bottom, separated by a neutral top border.
+ children.append(
+ html.Nav(
+ [
+ html.A(
+ [
+ _nav_icon(link["icon"]),
+ html.Span(link["title"], className="pc-nav-label"),
+ ],
+ href=link["url"],
+ target="_blank",
+ rel="noopener noreferrer",
+ className="pc-nav-item",
+ )
+ for link in HELP_LINKS
+ ],
+ className="pc-nav pc-footer",
+ )
+ )
+
+ return html.Div(children, className="pc-sidebar pc-font")
# Layout
dashboard.layout = html.Div(
[
- dcc.Location(id="url", refresh=False),
html.Link(rel="icon", href="assets/favicon.ico"),
- # Placeholder for dynamic navigation bar
html.Div(
[
+ # Dynamic sidebar (rebuilt on navigation for active state).
+ html.Div(id="navigation-bar"),
+ # Main pane hosting the routed page content.
html.Div(
- id="navigation-bar", className="bg-prowler-stone-900 min-w-36 z-10"
- ),
- html.Div(
- [
+ html.Div(
dash.page_container,
- ],
+ className="pc-main-inner",
+ ),
id="content_select",
- className="bg-prowler-white w-full col-span-11 h-screen mx-auto overflow-y-scroll no-scrollbar px-10 py-7",
+ className="pc-main pc-font no-scrollbar",
),
],
- className="grid custom-grid 2xl:custom-grid-large h-screen",
+ className="pc-shell",
),
],
className="h-screen mx-auto",
)
-# Callback to update navigation bar
-@dashboard.callback(Output("navigation-bar", "children"), [Input("url", "pathname")])
+# Callback to update navigation bar.
+#
+# Triggered off Dash Pages' own location (``_pages_location``) rather than a
+# separate ``dcc.Location``. A standalone ``dcc.Location(id="url")`` stops
+# emitting ``pathname`` when navigating between two pages that render an
+# identical component tree — every ``/cloud/*`` gated page shares the same
+# ``build_cloud_layout`` structure — which left the active highlight stuck on
+# the first gated page visited. ``_pages_location`` fires on every route change.
+@dashboard.callback(
+ Output("navigation-bar", "children"), [Input("_pages_location", "pathname")]
+)
def update_nav_bar(pathname):
- return html.Div(
- [
- html.Div([prowler_logo], className="mb-8 px-3"),
- html.H6(
- "Dashboards",
- className="px-3 text-prowler-stone-500 text-sm opacity-90 font-regular mb-2",
- ),
- html.Nav(
- [html.Ul(generate_nav_links(pathname), className="")],
- className="flex flex-col gap-y-6",
- ),
- html.Nav(
- [
- html.A(
- [
- html.Span(
- [
- html.Img(src="assets/favicon.ico", className="w-5"),
- "Subscribe to Prowler Cloud",
- ],
- className="flex items-center gap-x-3 text-white",
- ),
- ],
- href="https://prowler.com/",
- target="_blank",
- className="block p-3 uppercase text-xs hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime",
- ),
- html.Ul(generate_help_menu(), className=""),
- ],
- className="flex flex-col gap-y-6 mt-auto",
- ),
- ],
- className="flex flex-col bg-prowler-stone-900 py-7 h-full",
- )
+ return generate_sidebar(pathname)
diff --git a/dashboard/assets/cloud-pages.css b/dashboard/assets/cloud-pages.css
new file mode 100644
index 0000000000..53052eabda
--- /dev/null
+++ b/dashboard/assets/cloud-pages.css
@@ -0,0 +1,389 @@
+/*
+ * Prowler Local Dashboard — Cloud upsell chrome & gated pages.
+ * These styles are self-contained (not dependent on the precompiled Tailwind
+ * bundle) so pixel specs from the PRD render reliably without a rebuild.
+ */
+
+@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
+
+:root {
+ --pc-btn: #6ee7b7;
+ --pc-btn-hover: #99f6e4;
+ --pc-btn-press: #34d399;
+ --pc-grad-start: #2ee59b;
+ --pc-grad-end: #62dff0;
+ --pc-text: #020617;
+ --pc-text-2: #27272a;
+ --pc-sidebar-bg: #27272a;
+ --pc-pane-bg: #fdfdfd;
+ --pc-border: #e5e5e5;
+ --pc-teal-accent: #2ee59b;
+ --pc-sidebar-w: 264px;
+ --pc-sidebar-w-collapsed: 82px;
+}
+
+.pc-font {
+ font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto,
+ Helvetica, Arial, sans-serif;
+}
+
+/* ----------------------------------------------------------------- Shell */
+
+.pc-shell {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+ overflow: hidden;
+}
+
+.pc-main {
+ flex: 1 1 auto;
+ height: 100vh;
+ overflow-y: auto;
+ background: var(--pc-pane-bg);
+}
+
+.pc-main-inner {
+ padding: 28px 40px 64px;
+}
+
+/* --------------------------------------------------------------- Sidebar */
+
+.pc-sidebar {
+ flex: 0 0 var(--pc-sidebar-w);
+ width: var(--pc-sidebar-w);
+ background: var(--pc-sidebar-bg);
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ padding: 22px 0 16px;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.pc-sidebar::-webkit-scrollbar {
+ display: none;
+}
+
+.pc-brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 18px;
+ margin-bottom: 22px;
+}
+
+.pc-brand-lockup {
+ width: 190px;
+ height: auto;
+ display: block;
+}
+
+/* Compact mark is only revealed on the collapsed icon rail (see media query). */
+.pc-brand-mark {
+ width: 30px;
+ height: 30px;
+ flex: 0 0 auto;
+ display: none;
+}
+
+.pc-section {
+ color: #8a8a90;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ padding: 0 18px;
+ margin: 18px 0 8px;
+}
+
+.pc-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.pc-nav-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 9px 12px;
+ margin: 0 8px;
+ color: #ffffff;
+ font-size: 14px;
+ font-weight: 500;
+ text-decoration: none;
+ border-radius: 8px;
+ border-left: 3px solid transparent;
+ overflow: hidden;
+ transition: background 0.15s ease;
+}
+
+.pc-nav-item:hover {
+ background: rgba(255, 255, 255, 0.07);
+}
+
+.pc-nav-item.pc-active {
+ background: rgba(255, 255, 255, 0.09);
+ border-left-color: var(--pc-teal-accent);
+}
+
+.pc-nav-label {
+ flex: 0 1 auto;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Icon rendered as a recolorable mask so one asset serves any color. */
+.pc-ico {
+ width: 20px;
+ height: 20px;
+ flex: 0 0 auto;
+ display: inline-block;
+ background-color: currentColor;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ mask-position: center;
+ -webkit-mask-size: contain;
+ mask-size: contain;
+}
+
+.pc-pill {
+ flex: 0 0 auto;
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ background: linear-gradient(
+ 112deg,
+ var(--pc-grad-start) 3.5%,
+ var(--pc-grad-end) 98.8%
+ );
+ color: var(--pc-text);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ line-height: 1;
+ padding: 3px 8px;
+ border-radius: 9999px;
+ white-space: nowrap;
+}
+
+.pc-footer {
+ margin-top: auto;
+ padding-top: 12px;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* -------------------------------------------------------- Gated page body */
+
+.pc-page {
+ max-width: 1120px;
+ margin: 0 auto;
+}
+
+.pc-page-header {
+ border-bottom: 1px solid var(--pc-border);
+ padding-bottom: 18px;
+ margin-bottom: 28px;
+}
+
+.pc-page-title-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* In the page header the badge sits right next to the title (not pushed to the
+ far right like the sidebar pills) and is uppercased for emphasis. */
+.pc-page-title-row .pc-pill {
+ margin-left: 0;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.pc-page-title {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--pc-text);
+ margin: 0;
+}
+
+.pc-page-subtitle {
+ font-size: 15px;
+ line-height: 24px;
+ color: #52525b;
+ margin: 8px 0 0;
+}
+
+/* ----------------------------------------------------------- Upgrade card */
+
+.pc-card {
+ position: relative;
+ background: #ffffff;
+ border: 1px solid var(--pc-border);
+ border-radius: 18px;
+ padding: 56px 40px;
+ overflow: hidden;
+}
+
+.pc-card-glow {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 560px;
+ height: 240px;
+ background: radial-gradient(
+ ellipse at top,
+ rgba(46, 229, 155, 0.2),
+ rgba(98, 223, 240, 0.06) 45%,
+ transparent 72%
+ );
+ pointer-events: none;
+}
+
+.pc-card-body {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.pc-feature-icon {
+ width: 64px;
+ height: 64px;
+ border-radius: 16px;
+ background: linear-gradient(
+ 135deg,
+ rgba(46, 229, 155, 0.16),
+ rgba(98, 223, 240, 0.14)
+ );
+ border: 1px solid rgba(46, 229, 155, 0.3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--pc-text);
+ margin-bottom: 22px;
+}
+
+.pc-feature-icon .pc-ico {
+ width: 30px;
+ height: 30px;
+}
+
+.pc-avail {
+ color: #0e9f6e;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+ margin-bottom: 8px;
+}
+
+.pc-card-title {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--pc-text);
+ margin: 0 0 14px;
+}
+
+.pc-card-desc {
+ font-size: 15px;
+ line-height: 24px;
+ color: var(--pc-text-2);
+ max-width: 560px;
+ margin: 0 0 26px;
+}
+
+.pc-benefits {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 30px;
+ text-align: left;
+}
+
+.pc-benefit {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 7px 0;
+ font-size: 15px;
+ line-height: 22px;
+ color: var(--pc-text-2);
+}
+
+.pc-benefit-check {
+ flex: 0 0 auto;
+ width: 18px;
+ height: 18px;
+ margin-top: 2px;
+ color: var(--pc-btn-press);
+}
+
+.pc-cta {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--pc-btn);
+ color: var(--pc-text);
+ font-size: 15px;
+ font-weight: 700;
+ padding: 12px 24px;
+ border-radius: 12px;
+ border: 1px solid var(--pc-btn-press);
+ text-decoration: none;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.pc-cta:hover {
+ background: var(--pc-btn-hover);
+ color: var(--pc-text);
+}
+
+.pc-cta:active {
+ background: var(--pc-btn-press);
+}
+
+/* --------------------------------------------------- Responsive collapse */
+
+@media (max-width: 900px) {
+ .pc-sidebar {
+ flex-basis: var(--pc-sidebar-w-collapsed);
+ width: var(--pc-sidebar-w-collapsed);
+ }
+
+ .pc-brand {
+ justify-content: center;
+ padding: 0 8px;
+ }
+
+ .pc-brand-lockup {
+ display: none;
+ }
+
+ .pc-brand-mark {
+ display: block;
+ }
+
+ .pc-section,
+ .pc-nav-label,
+ .pc-pill {
+ display: none;
+ }
+
+ .pc-nav-item {
+ justify-content: center;
+ margin: 0 6px;
+ padding: 10px 0;
+ }
+
+ .pc-main-inner {
+ padding: 20px 18px 48px;
+ }
+}
diff --git a/dashboard/assets/images/icons/cloud/alerts.svg b/dashboard/assets/images/icons/cloud/alerts.svg
new file mode 100644
index 0000000000..6109e378ab
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/alerts.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/attack-paths.svg b/dashboard/assets/images/icons/cloud/attack-paths.svg
new file mode 100644
index 0000000000..b856c6ea2f
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/attack-paths.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/check.svg b/dashboard/assets/images/icons/cloud/check.svg
new file mode 100644
index 0000000000..a5f8ed0c8a
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/check.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/compliance.svg b/dashboard/assets/images/icons/cloud/compliance.svg
new file mode 100644
index 0000000000..e70ebda28c
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/compliance.svg
@@ -0,0 +1,4 @@
+
diff --git a/dashboard/assets/images/icons/cloud/docs.svg b/dashboard/assets/images/icons/cloud/docs.svg
new file mode 100644
index 0000000000..efc3dfc61b
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/docs.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/findings.svg b/dashboard/assets/images/icons/cloud/findings.svg
new file mode 100644
index 0000000000..93fc42a516
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/findings.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/help.svg b/dashboard/assets/images/icons/cloud/help.svg
new file mode 100644
index 0000000000..2b3f1024e8
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/help.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/integrations.svg b/dashboard/assets/images/icons/cloud/integrations.svg
new file mode 100644
index 0000000000..e7a2d07603
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/integrations.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/lighthouse-ai.svg b/dashboard/assets/images/icons/cloud/lighthouse-ai.svg
new file mode 100644
index 0000000000..1c510ac1fc
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/lighthouse-ai.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/mutelist.svg b/dashboard/assets/images/icons/cloud/mutelist.svg
new file mode 100644
index 0000000000..1d498fa9e3
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/mutelist.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/organization.svg b/dashboard/assets/images/icons/cloud/organization.svg
new file mode 100644
index 0000000000..1f3d1da9f5
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/organization.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/overview.svg b/dashboard/assets/images/icons/cloud/overview.svg
new file mode 100644
index 0000000000..809e63d945
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/overview.svg
@@ -0,0 +1,4 @@
+
diff --git a/dashboard/assets/images/icons/cloud/prowler-lockup.svg b/dashboard/assets/images/icons/cloud/prowler-lockup.svg
new file mode 100644
index 0000000000..9daee36463
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/prowler-lockup.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/assets/images/icons/cloud/prowler-mark.svg b/dashboard/assets/images/icons/cloud/prowler-mark.svg
new file mode 100644
index 0000000000..eb30d44be8
--- /dev/null
+++ b/dashboard/assets/images/icons/cloud/prowler-mark.svg
@@ -0,0 +1 @@
+
diff --git a/dashboard/lib/layouts.py b/dashboard/lib/layouts.py
index 3fb230f314..6d1e7947fa 100644
--- a/dashboard/lib/layouts.py
+++ b/dashboard/lib/layouts.py
@@ -156,7 +156,7 @@ def create_layout_compliance(
html.Img(src="assets/favicon.ico", className="w-5 mr-3"),
html.Span("Subscribe to Prowler Cloud"),
],
- href="https://cloud.prowler.com/",
+ href="https://cloud.prowler.com/sign-up?utm_source=prowler-local-dashboard&utm_content=compliance",
target="_blank",
className="text-prowler-stone-900 inline-flex px-4 py-2 text-xs font-bold uppercase transition-all rounded-lg text-gray-900 hover:bg-prowler-stone-900/10 border-solid border-1 hover:border-prowler-stone-900/10 hover:border-solid hover:border-1 border-prowler-stone-900/10",
),
diff --git a/dashboard/pages/cloud.py b/dashboard/pages/cloud.py
new file mode 100644
index 0000000000..49778f8e26
--- /dev/null
+++ b/dashboard/pages/cloud.py
@@ -0,0 +1,265 @@
+"""Prowler Cloud upsell (gated) informational pages.
+
+These routes live inside the Local Dashboard but do NOT reproduce any Prowler
+Cloud functionality. Each renders the same reusable upgrade template with a
+feature-specific name, icon, description, benefit bullets and UTM-tagged CTA.
+All copy in ``CLOUD_FEATURES`` is normative — do not change wording,
+capitalization or punctuation without Product approval.
+"""
+
+import dash
+from dash import html
+
+# Shared subtitle used across every gated page header.
+CLOUD_SUBTITLE = "Discover more ways to protect and operate your cloud."
+
+# Base Prowler Cloud URL; the UTM content value identifies the feature.
+CLOUD_CTA_BASE = (
+ "https://cloud.prowler.com/sign-up?utm_source=prowler-local-dashboard&utm_content="
+)
+
+# Path to the recolorable checkmark mask used for benefit bullets.
+CHECK_ICON = "/assets/images/icons/cloud/check.svg"
+
+
+# Normative feature definitions. ``icon`` points to a local mask asset so no
+# external requests are needed and the glyph recolors per context.
+CLOUD_FEATURES = [
+ {
+ "slug": "lighthouse-ai",
+ "route": "/cloud/lighthouse-ai",
+ "nav_label": "Lighthouse AI",
+ "page_title": "Lighthouse AI",
+ "card_title": "Unlock Lighthouse AI",
+ "description": (
+ "Work with an AI security analyst that understands your cloud "
+ "posture and helps turn risk into action."
+ ),
+ "benefits": [
+ "Ask questions about your security posture in plain language",
+ "Investigate findings with context from your connected providers",
+ "Move from insight to remediation faster",
+ ],
+ "utm_content": "lighthouse-ai",
+ "icon": "/assets/images/icons/cloud/lighthouse-ai.svg",
+ },
+ {
+ "slug": "attack-paths",
+ "route": "/cloud/attack-paths",
+ "nav_label": "Attack Paths",
+ "page_title": "Attack Paths",
+ "card_title": "Unlock Attack Paths",
+ "description": (
+ "Visualize the paths an attacker could take through connected "
+ "resources before risk becomes compromise."
+ ),
+ "benefits": [
+ "See exploitable relationships across your AWS environment",
+ "Focus remediation on the paths with the greatest impact",
+ "Explore each scan as a point-in-time security graph",
+ ],
+ "utm_content": "attack-paths",
+ "icon": "/assets/images/icons/cloud/attack-paths.svg",
+ },
+ {
+ "slug": "findings",
+ "route": "/cloud/findings",
+ "nav_label": "Findings",
+ "page_title": "Findings",
+ "card_title": "Unlock Findings",
+ "description": (
+ "Filter, investigate, and prioritize security findings across "
+ "providers and scans from one workspace."
+ ),
+ "benefits": [
+ "Search and filter findings across all connected accounts",
+ "Track status, severity, ownership, and remediation context",
+ "Triage findings and share a consistent source of truth with your security team",
+ ],
+ "utm_content": "findings",
+ "icon": "/assets/images/icons/cloud/findings.svg",
+ },
+ {
+ "slug": "alerts",
+ "route": "/cloud/alerts",
+ "nav_label": "Alerts",
+ "page_title": "Alerts",
+ "card_title": "Unlock Alerts",
+ "description": (
+ "Create alert rules and stay informed when scan results reveal "
+ "the risks your team cares about."
+ ),
+ "benefits": [
+ "Define alerts around the findings that matter most",
+ "Route security signals to the right responders",
+ "Reduce the time between detection and action",
+ ],
+ "utm_content": "alerts",
+ "icon": "/assets/images/icons/cloud/alerts.svg",
+ },
+ {
+ "slug": "mutelist",
+ "route": "/cloud/mutelist",
+ "nav_label": "Mutelist",
+ "page_title": "Mutelist",
+ "card_title": "Unlock Mutelist",
+ "description": (
+ "Quiet expected findings, document accepted risk, and keep your "
+ "team focused on actionable work."
+ ),
+ "benefits": [
+ "Create reusable rules for known exceptions",
+ "Keep muted findings available for audit and review",
+ "Cut noise without losing security context",
+ ],
+ "utm_content": "mutelist",
+ "icon": "/assets/images/icons/cloud/mutelist.svg",
+ },
+ {
+ "slug": "integrations",
+ "route": "/cloud/integrations",
+ "nav_label": "Integrations",
+ "page_title": "Integrations",
+ "card_title": "Unlock Integrations",
+ "description": (
+ "Connect Prowler to your security workflow so findings and scan "
+ "data reach the tools your team already uses."
+ ),
+ "benefits": [
+ "Connect ticketing, notification, and cloud security services",
+ "Automate the handoff from detection to response",
+ "Keep teams aligned without manual exports",
+ ],
+ "utm_content": "integrations",
+ "icon": "/assets/images/icons/cloud/integrations.svg",
+ },
+ {
+ "slug": "organization",
+ "route": "/cloud/organization",
+ "nav_label": "Organization",
+ "page_title": "Organization",
+ "card_title": "Unlock Organization",
+ "description": (
+ "Manage users, roles, and invitations while organizing cloud "
+ "security work across your team."
+ ),
+ "benefits": [
+ "Invite teammates into a shared security workspace",
+ "Control access with role-based permissions",
+ "Coordinate security operations across accounts and teams",
+ ],
+ "utm_content": "organization",
+ "icon": "/assets/images/icons/cloud/organization.svg",
+ },
+]
+
+# Convenience lookup for the navigation builder in ``__main__``.
+CLOUD_FEATURES_BY_SLUG = {feature["slug"]: feature for feature in CLOUD_FEATURES}
+
+
+def _mask_style(icon_url):
+ """Return the inline style that renders a recolorable mask icon."""
+ return {
+ "WebkitMaskImage": f"url({icon_url})",
+ "maskImage": f"url({icon_url})",
+ }
+
+
+def _benefit_item(text):
+ return html.Li(
+ [
+ html.Span(
+ className="pc-ico pc-benefit-check",
+ style=_mask_style(CHECK_ICON),
+ ),
+ html.Span(text),
+ ],
+ className="pc-benefit",
+ )
+
+
+def build_cloud_layout(feature):
+ """Build the reusable gated informational page for a single feature."""
+ cta_url = f"{CLOUD_CTA_BASE}{feature['utm_content']}"
+
+ return html.Div(
+ html.Div(
+ [
+ # Page header: title + Prowler Cloud badge + shared subtitle.
+ html.Div(
+ [
+ html.Div(
+ [
+ html.H1(
+ feature["page_title"],
+ className="pc-page-title",
+ ),
+ html.Span("Prowler Cloud", className="pc-pill"),
+ ],
+ className="pc-page-title-row",
+ ),
+ html.P(CLOUD_SUBTITLE, className="pc-page-subtitle"),
+ ],
+ className="pc-page-header",
+ ),
+ # Centered upgrade card.
+ html.Div(
+ [
+ html.Div(className="pc-card-glow"),
+ html.Div(
+ [
+ html.Div(
+ html.Span(
+ className="pc-ico",
+ style=_mask_style(feature["icon"]),
+ ),
+ className="pc-feature-icon",
+ ),
+ html.Div(
+ "Available in Prowler Cloud",
+ className="pc-avail",
+ ),
+ html.H2(
+ feature["card_title"],
+ className="pc-card-title",
+ ),
+ html.P(
+ feature["description"],
+ className="pc-card-desc",
+ ),
+ html.Ul(
+ [
+ _benefit_item(benefit)
+ for benefit in feature["benefits"]
+ ],
+ className="pc-benefits",
+ ),
+ html.A(
+ "Upgrade to Prowler Cloud",
+ href=cta_url,
+ target="_blank",
+ rel="noopener noreferrer",
+ className="pc-cta",
+ ),
+ ],
+ className="pc-card-body",
+ ),
+ ],
+ className="pc-card",
+ ),
+ ],
+ className="pc-page pc-font",
+ ),
+ )
+
+
+# Register one page per gated feature. A distinct module key keeps each entry
+# unique in Dash's page registry while sharing the same template.
+for _feature in CLOUD_FEATURES:
+ dash.register_page(
+ f"cloud_{_feature['slug'].replace('-', '_')}",
+ path=_feature["route"],
+ name=_feature["nav_label"],
+ title=f"Prowler Dashboard - {_feature['page_title']}",
+ layout=build_cloud_layout(_feature),
+ )
diff --git a/dashboard/pages/overview.py b/dashboard/pages/overview.py
index e705f15e9f..8f786412d9 100644
--- a/dashboard/pages/overview.py
+++ b/dashboard/pages/overview.py
@@ -1538,7 +1538,7 @@ def filter_data(
html.Img(src="assets/favicon.ico", className="w-5 mr-3"),
html.Span("Subscribe to Prowler Cloud"),
],
- href="https://cloud.prowler.com/",
+ href="https://cloud.prowler.com/sign-up?utm_source=prowler-local-dashboard&utm_content=overview",
target="_blank",
className="text-prowler-stone-900 inline-flex px-4 py-2 text-xs font-bold uppercase transition-all rounded-lg text-gray-900 hover:bg-prowler-stone-900/10 border-solid border-1 hover:border-prowler-stone-900/10 hover:border-solid hover:border-1 border-prowler-stone-900/10",
),
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index d737298183..6f7c5a3ff4 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -64,7 +64,7 @@ services:
condition: service_healthy
postgres:
- image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
+ image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -88,7 +88,7 @@ services:
retries: 5
valkey:
- image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
+ image: valkey/valkey:8-alpine@sha256:a038175878d66b9d274fbf8be73c0305e93798b83917647f167e18cef3c71eec
hostname: "valkey"
volumes:
- ./_data/valkey:/data
@@ -104,7 +104,7 @@ services:
retries: 3
neo4j:
- image: graphstack/dozerdb:5.26.3.0@sha256:a77526ea3918fdc46d1fff70c4aea7d71d3874a26ecec059179d6775845b1247
+ image: graphstack/dozerdb:5.26.27.0@sha256:9b54d6b3a98a76c00bd23e8e78d8c82081ff168162aebd47b25c234e092cb0a0
hostname: "neo4j"
volumes:
- ./_data/neo4j:/data
diff --git a/docker-compose.yml b/docker-compose.yml
index 6cb5ac237d..5ed0e97a28 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -60,7 +60,7 @@ services:
start_period: 60s
postgres:
- image: postgres:16.3-alpine3.20@sha256:36ed71227ae36305d26382657c0b96cbaf298427b3f1eaeb10d77a6dea3eec41
+ image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
@@ -80,7 +80,7 @@ services:
retries: 5
valkey:
- image: valkey/valkey:7-alpine3.19@sha256:4054fe7fc607b9326ac7c4691ed26e9670d2ff17a9fb28c2577adecf928acbcc
+ image: valkey/valkey:8-alpine@sha256:a038175878d66b9d274fbf8be73c0305e93798b83917647f167e18cef3c71eec
hostname: "valkey"
volumes:
- ./_data/valkey:/data
@@ -96,7 +96,7 @@ services:
retries: 3
neo4j:
- image: graphstack/dozerdb:5.26.3.0@sha256:a77526ea3918fdc46d1fff70c4aea7d71d3874a26ecec059179d6775845b1247
+ image: graphstack/dozerdb:5.26.27.0@sha256:9b54d6b3a98a76c00bd23e8e78d8c82081ff168162aebd47b25c234e092cb0a0
hostname: "neo4j"
volumes:
- ./_data/neo4j:/data
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index 8278a7f88a..45bd3f04fa 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -78,11 +78,25 @@ b. National security is of the utmost concern nowadays.
Prowler Features are considered proper nouns. They are to be referenced without articles in all pieces of writing.
-This is a list of Prowler Features:
+Prowler ships two product families. Use these names exactly; the former names Prowler App (now Prowler Local Server) and Prowler Enterprise (now Prowler Private Cloud) must not appear in new writing. The only allowed former-name notes are on the Prowler Product Families page (`getting-started/products/index.mdx`) and in the site-wide banner, which document the mapping.
+
+Prowler Products:
+
+* **Prowler Cloud**
+* **Prowler Private Cloud** (formerly Prowler Enterprise)
+* **Prowler Hub**
+* **Prowler Lighthouse AI**
+* **Prowler MCP**
+
+Open Source projects:
-* **Prowler App**
* **Prowler CLI**
+* **Prowler Local Server** (formerly Prowler App)
+* **Prowler Local Dashboard** (the Prowler CLI dashboard)
* **Prowler SDK**
+
+Other Prowler Features:
+
* **Built-in Compliance Checks**
* **Multi-cloud Security Scanning**
* **Autonomous Cloud Security Analyst (AI)**
@@ -97,8 +111,6 @@ This is a list of Prowler Features:
* **AI-Generated Detections & Remediations**
* **Prowler Studio**
* **Custom Security Policies**
-* **Prowler Cloud**
-* **Prowler Registry**
* **Open Source & Full APIs**
---
@@ -137,10 +149,10 @@ Explicit use of second-person pronouns (you) and possessives (your) should be mi
### Example of Improvement Through Avoiding Second Person Pronouns
**Original:**
-Prowler App can be installed in different ways, depending on your environment:
+Prowler Local Server can be installed in different ways, depending on your environment:
**Improved Version:**
-Prowler App offers flexible installation methods tailored to various environments:
+Prowler Local Server offers flexible installation methods tailored to various environments:
---
@@ -262,7 +274,7 @@ There are several options for punctuating bullet points. Regardless of the style
* **No punctuation (minimalistic):** This strategy is suitable when no verbs are involved and is best used to highlight products or features in isolation. For example:
- Prowler App is composed of three key components:
+ Prowler Local Server is composed of three key components:
* Prowler UI
* Prowler API
* Prowler SDK
@@ -271,7 +283,7 @@ There are several options for punctuating bullet points. Regardless of the style
* **Periods for full sentences:** This approach works best when each bullet point forms a full sentence or includes verbs. For example:
- Prowler App is composed of three key components:
+ Prowler Local Server is composed of three key components:
* Prowler UI, a web-based interface, built with Next.js, providing a user-friendly experience for executing Prowler scans and visualizing results.
* Prowler API, a backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
* Prowler SDK, a Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
@@ -539,6 +551,43 @@ Tag-Based Scanning allows filtering resources by AWS tags during security assess
---
+## AppliesTo Banner for Product Scope
+
+The AppliesTo component states which products a guide covers and links to the product families page. It is located at `docs/snippets/applies-to.mdx`.
+
+### When to Use the AppliesTo Banner
+
+Use it on web UI tutorial pages that apply to more than one product (for example, a guide written for Prowler Cloud whose steps also work on Prowler Local Server). Do not combine it with the SubscriptionBanner: pages carrying the SubscriptionBanner already state their availability.
+
+### How to Use the AppliesTo Banner
+
+```mdx
+import { AppliesTo } from "/snippets/applies-to.mdx"
+
+
+```
+
+The default covers Prowler Cloud, Prowler Private Cloud, and Prowler Local Server. Pass the `products` prop to narrow the scope:
+
+```mdx
+
+```
+
+Place it on its own line below the Version Badge when one is present, otherwise directly after the imports.
+
+### Cloud Marker for Subscription Content
+
+The custom green cloud glyph (`docs/images/icons/cloud-bold.svg`) marks content that requires a Prowler Cloud or Prowler Private Cloud subscription. It renders as a trailing `::after` element through the "Cloud marker" rules in `docs/style.css`, so sidebar labels stay left-aligned.
+
+* Pages: add a `li[id=""] a span::after` selector to the Cloud marker rule in `docs/style.css` for every page whose content is subscription-gated. The `li` id equals the page URL path. Pages where only one section is gated (for example the Support page, where only the Support Desk carries the SubscriptionBanner) get no marker.
+* Navigation groups: add a selector only when every page in the group is subscription-gated. One sanctioned exception: the Prowler MCP group is marked because the hosted server at `mcp.prowler.com` includes tools for Prowler Cloud-specific features, and its overview page explains the free local alternative. Nested groups render as `li[data-title=""]` with a button toggle, so use `li[data-title=""] > button span:first-child::after`. Top-level groups render as `h3` headings without `data-title`, which means name collisions with top-level groups resolve themselves; a gated top-level group (always expanded) is selected through its sibling list with `:has()`, like the Security tab group. Do not use `:has()` on child links of nested groups: collapsed groups do not render their children.
+
+Do not use the `icon` field for this marker (icons render before the label and misalign the sidebar), and do not use `"tag"` on navigation groups (group-level tags crash `mint broken-links` with a stack overflow, verified with mint 4.2.689). The SVG stroke uses the fixed brand green `#10B981` on purpose: it must be visible on both themes without `currentColor` support.
+
+The marker meaning is explained on the Prowler Product Families page at `getting-started/products/index.mdx`: keep that note in place.
+
+---
+
## Avoid Assumptions Regarding Audience’s Expertise
### Understand Your Audience’s Expertise
diff --git a/docs/README.md b/docs/README.md
index 595f79bc5a..7934972c9b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,7 +5,7 @@ This repository contains the Prowler Open Source documentation powered by [Mintl
## Documentation Structure
- **Getting Started**: Overview, installation, and basic usage guides
-- **User Guide**: Comprehensive guides for Prowler App, CLI, providers, and compliance
+- **User Guide**: Comprehensive guides for Prowler Cloud, Prowler Local Server, Prowler CLI, providers, and compliance
- **Developer Guide**: Technical documentation for developers contributing to Prowler
## Local Development
@@ -13,7 +13,7 @@ This repository contains the Prowler Open Source documentation powered by [Mintl
Install a reviewed version of the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview documentation changes locally:
```bash
-npm install --global mint@4.2.560
+npm install --global mint@4.2.689
```
Run the following command at the root of your documentation (where `mint.json` is located):
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
new file mode 100644
index 0000000000..2233ee1077
--- /dev/null
+++ b/docs/changelog.mdx
@@ -0,0 +1,878 @@
+---
+title: "Changelog"
+description: "New features and improvements in each Prowler release"
+rss: true
+---
+
+
+ ### 📌 Compliance Watchlist
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Compliance Watchlist keeps the frameworks an organization tracks in one shared list. Pin frameworks from any compliance view, manage several at once through a searchable catalog, and filter the Compliance section to show only the pinned frameworks.
+
+ The Overview page now reports the latest score for every pinned framework, while finding details highlight the watched frameworks associated with each check. Universal frameworks remain a single watchlist entry across provider views, keeping the organization's priorities consistent everywhere.
+
+ 
+
+ Read more in the [Compliance Watchlist documentation](https://docs.prowler.com/user-guide/compliance/tutorials/compliance#tracking-frameworks-with-the-compliance-watchlist).
+
+ ### 🔐 SAML SSO - Multiple Email Domains
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ One SAML configuration can now authorize a primary email domain and up to 19 additional domains through the same Identity Provider. Every domain shares one stable Assertion Consumer Service (ACS) URL based on the primary domain, so subsidiaries, acquired companies, regional domains, and multiple brands no longer require separate tenants or duplicated SAML applications.
+
+ Domain ownership remains tenant-bound throughout the authentication flow. During service provider-initiated sign-in, the discovery domain and the domain asserted by the Identity Provider must resolve to the same tenant before provisioning continues.
+
+ 
+
+ Read more in the [SAML SSO documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-sso#add-multiple-saml-domains).
+
+ ### 👥 User Sign-In Methods
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ The Users table now shows each account's sign-in methods as tags, including email/password, Google, GitHub, SAML with linked domains, and Partner SSO. Accounts without a reported method display a placeholder.
+
+ 
+
+ ### 🕸️ Attack Paths - Expanded AWS Privilege-Escalation Coverage
+
+ Attack Paths adds 20 AWS privilege-escalation queries from [pathfinding.cloud](https://pathfinding.cloud), while `iam_policy_allows_privilege_escalation` gains 22 additional escalation combinations.
+
+ The new coverage includes service `iam:PassRole` paths across AWS Batch, Braket, Cognito Identity, ECS, EMR, EMR Serverless, GameLift, Glue, EC2 Image Builder, Kinesis Analytics, HealthOmics, EventBridge Scheduler, Systems Manager, and Step Functions. It also covers existing-resource abuse, permissions-boundary removal, role assumption, and IAM Identity Center permission-set policy injection.
+
+ The query catalog now exposes each AWS query's outcome category, distinguishing code execution, privilege escalation, public exposure, and resource inventory.
+
+ Explore the full Attack Paths query catalog at [Prowler Hub](https://hub.prowler.com/attack-paths).
+
+ Read more in the [Attack Paths documentation](https://docs.prowler.com/user-guide/tutorials/prowler-app-attack-paths).
+
+ ### 🔍 Checks
+
+ #### Microsoft 365
+
+ Twelve new checks expand the coverage of CIS Microsoft 365 Foundations Benchmark v7.0.0:
+
+ - **Admin Center:** Shared Bookings is disabled.
+ - **Defender:** Priority account protection and strict preset security policies are enabled.
+ - **Entra ID:** Six checks cover device registration restrictions, local administrator behavior, device limits, LAPS, and BitLocker key visibility.
+ - **Exchange Online:** Personal accounts in Outlook on the web are disabled and Direct Send is rejected.
+ - **Microsoft Teams:** External access from trial-only tenants is blocked.
+
+ Explore all Microsoft 365 checks at [Prowler Hub](https://hub.prowler.com/check?provider=m365).
+
+ ### 🔐 Security
+
+ - Prowler API, UI, SDK, and MCP container images now publish per-architecture Software Bills of Materials (SBOMs) and build-provenance attestations. Prowler Cloud production and Prowler Private Cloud images carry the same attestations.
+ - SDK and API container builds verify the checksums of downloaded PowerShell, Trivy, and zizmor binaries before installation.
+ - Grype now complements Trivy across the container-image security gates, detecting components and vulnerabilities that manifest-based scanners can miss and blocking fixable high and critical findings.
+ - `aiohttp` was upgraded to 3.14.3 to address CVE-2026-69244. `cryptography` was upgraded to 50.0.0 to address CVE-2026-69247 and CVE-2026-69249.
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.38.0) for the complete list of changes.
+
+
+
+ ### 💬 Lighthouse AI — Context-Aware Chat and a Bigger Toolbox
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Lighthouse AI is now aware of your working context when in Prowler Cloud. Messages carry page-aware context — the page you are on, the finding or resource open in the side panel, and its metadata — so "explain this" just works, and each page offers concise contextual suggestions to start from.
+
+ 
+
+ Lighthouse also gained access to every tool family the Prowler MCP server advertises: scan configurations, scan scheduling, finding triage, alert rules and recipients, integrations, users, and roles. Every action remains gated by RBAC: Lighthouse AI can only do what the user asking could do themselves.
+
+ Read more in the [Lighthouse AI documentation](/getting-started/products/prowler-cloud-lighthouse).
+
+ ### 🔌 Prowler MCP — Integrations, Users, and Roles
+
+ Prowler MCP gained three tool families, available on both the Cloud and the self-hosted Local MCP Server:
+
+ - **[Integrations](/getting-started/basic-usage/prowler-mcp-tools#integrations-management)** — manage where Prowler sends its results, with the full lifecycle for Amazon S3, AWS Security Hub, and Jira: create them, update credentials, configuration and attached providers, re-check connections, and delete them — plus turning findings into Jira work items directly from a conversation.
+ - **[Users](/getting-started/basic-usage/prowler-mcp-tools#user-management)** — read-only tools to list the tenant users with their emails and identify the authenticated user.
+ - **[Roles](/getting-started/basic-usage/prowler-mcp-tools#role-management)** — browse the RBAC roles defined in the tenant, inspect the capabilities each one grants, and set the role a user holds.
+
+ ### ☁️ Prowler MCP — Cloud-Only Tools
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing). These tools are exposed only by the Cloud MCP Server at `https://mcp.prowler.com/mcp`; the self-hosted Local MCP Server **does not** include them.
+
+
+ A new `prowler_cloud_*` namespace adds 32 tools so your AI assistant can run Prowler Cloud workflows end to end instead of only reading from them:
+
+ - **[Alerts](/getting-started/basic-usage/prowler-mcp-tools#alerts)** — create and manage alert rules and email recipients, and browse the fired-alert history. Rule conditions can be dry-run before saving, so you can see what a rule would match without persisting anything.
+ - **[Findings Triage](/getting-started/basic-usage/prowler-mcp-tools#findings-triage)** — set a finding's triage status and attach notes documenting the decision. Unlike muting, the finding stays visible.
+ - **[Scan Scheduling](/getting-started/basic-usage/prowler-mcp-tools#scan-scheduling)** — configure daily, interval, weekly, or monthly recurring scans, one provider at a time or applied across many at once.
+ - **[Scan Configurations](/getting-started/basic-usage/prowler-mcp-tools#scan-configurations)** — build reusable check and compliance selections and attach them to providers.
+
+ Read more in the [Prowler MCP tools reference](/getting-started/basic-usage/prowler-mcp-tools#prowler-cloud-tools).
+
+ ### 🧭 Compliance — Grouped by provider of the same type
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ One framework, every provider, a single answer. Building on the cross-provider-type roll-up, the Compliance section now groups compliance for all providers of the same type: a **single-provider framework** — CIS AWS, CIS GCP, ENS for Azure — is aggregated across the latest completed scan of every provider of that type. Each framework card rolls up into a consolidated posture with a per-provider breakdown, a findings drill-down, and a combined executive PDF report. Requirement status follows the same strict precedence (FAIL over PASS over MANUAL), so one failing provider flags the requirement for the whole estate.
+
+ 
+
+ The Compliance tabs were also renamed to say what they aggregate: "Per Scan" is now **Single Scan**, "Cross-Provider" is now **Multiple Scans**, and Compliance lands on Multiple Scans by default.
+
+ 
+
+ Read more in the [Cross-Provider Compliance documentation](/user-guide/compliance/tutorials/cross-provider-compliance).
+
+ ### ☁️ GCP Organization Onboarding
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Onboarding an entire Google Cloud organization is now a single guided flow. Provide an organization-level credential and Prowler discovers the full hierarchy, every folder and project. Pick the folders and projects to onboard from a selection tree, set custom aliases, test the connection, and launch: each selected project is registered as a provider, with no need to add them one by one. Post-onboarding management is covered too, including credential replacement and organization-wide deletion.
+
+ Read more in the [GCP Organizations documentation](/user-guide/tutorials/prowler-cloud-gcp-organizations).
+
+ ### 🕸️ Attack Paths — More Privilege Escalation Queries
+
+ Attack Paths adds four AWS privilege-escalation detection queries from [pathfinding.cloud](https://pathfinding.cloud). Thanks to @paramanandmallik!
+
+ - **[STS-002](https://hub.prowler.com/attack-paths/aws-sts-privesc-cross-account-trust)** — cross-account role trust
+ - **[STS-003](https://hub.prowler.com/attack-paths/aws-sts-privesc-wildcard-trust)** — wildcard role trust
+ - **[IAM-022](https://hub.prowler.com/attack-paths/aws-iam-privesc-delete-user-permissions-boundary)** — user permissions-boundary removal
+ - **[SSO-001](https://hub.prowler.com/attack-paths/aws-sso-privesc-permission-set-escalation)** — IAM Identity Center permission-set escalation
+
+ The query info panel now links every query to its page on [Prowler Hub](https://hub.prowler.com), and the IAM privilege-escalation queries were reworked to run efficiently on accounts with many IAM roles, users, or groups, fixing runtime errors and timeouts on large graphs.
+
+ Read more in the [Attack Paths documentation](/user-guide/tutorials/prowler-app-attack-paths).
+
+ ### 🛡️ AWS Confidential Computing — Nitro Enclaves Checks
+
+ Prowler adds the first CSPM coverage for confidential computing workloads on AWS, with **11 new checks** for [Nitro Enclaves](https://aws.amazon.com/ec2/nitro/nitro-enclaves/), developed together with [Guillermo Ruiz](https://www.linkedin.com/in/gruizesteban/) from AWS.
+
+ - **Workload host environment (EC2)** — five `ec2_confidential_workload_host_*` checks for the parent instance: IMDSv2 not enforced, public IP exposure, unrestricted ingress, exposed vsock proxy ports, and hosts not running.
+ - **KMS attestation policy** — six `kms_key_enclave_*` checks for the key policies gating enclave secrets: attestation not enforced or bypassable, missing deployment binding, debug-mode attestations, PCR mismatches, and unknown enclave images.
+
+ All checks are fully passive, using AWS APIs and CloudTrail with no instance access or SSM agent required, and are mapped across 23 compliance frameworks, including NIST 800-53 Rev 5, PCI-DSS v4.0, ISO 27001:2022, SOC 2, HIPAA, and MITRE ATT&CK.
+
+ Read more about it this [blog post](https://prowler.com/blog/your-llm-runs-in-a-nitro-enclave-who-is-checking-the-enclave).
+
+ Try them out now at [cloud.prowler.com](https://cloud.prowler.com/sign-up)!
+
+ ### 🏢 New Provider — Huawei Cloud
+
+ Prowler now scans [**Huawei Cloud**](https://www.huaweicloud.com/), with **25 checks** across ten services: CTS, ECS, ELB, EVS, IAM, KMS, OBS, RDS, VPC, and WAF, plus the CIS Huawei Cloud Foundations Benchmark 1.0 compliance framework. Thanks to @tomitobio for their 1st provider in Prowler!
+
+ To scan a Huawei Cloud account, export the IAM user's access key credentials and run Prowler CLI:
+
+ ```bash
+ export HUAWEICLOUD_ACCESS_KEY_ID="your-access-key-id"
+ export HUAWEICLOUD_SECRET_ACCESS_KEY="your-secret-access-key"
+
+ prowler huaweicloud
+ ```
+
+ Read more in the [Huawei Cloud documentation](/user-guide/providers/huaweicloud/getting-started-huaweicloud). Explore all Huawei Cloud checks at [Prowler Hub](https://hub.prowler.com/check?provider=huaweicloud).
+
+ ### 🔍 Checks
+
+ #### AWS
+
+ - `codecommit_repository_no_secrets`, alongside the new `codecommit` service, scans files tracked at the tip of each repository's default branch for hardcoded secrets. Thanks to @Sid-0602!
+ - `glue_catalog_connection_no_secrets` detects secrets in Glue Data Catalog connection properties. Thanks to @l46983284-cpu, @Rishi943, and @UTKARSH698!
+ - `ec2_instance_stopped_older_than_specific_days` detects EC2 instances stopped longer than a configurable number of days (default 30). Thanks to @Nithin078!
+ - `sagemaker_endpoint_config_kms_encryption_enabled` verifies SageMaker endpoint configurations use a KMS key for storage volume encryption. Thanks to @Nithin078 and @l46983284-cpu!
+
+ Read more in the [AWS documentation](/user-guide/providers/aws/getting-started-aws). Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
+
+ ### 📤 OCSF Output — MITRE ATT&CK Enrichment
+
+ OCSF detection finding output now populates `finding_info.analytic` with the Prowler check rule and `finding_info.attacks` with MITRE ATT&CK technique and tactic objects for findings with MITRE ATT&CK compliance metadata. Thanks to @AlexanderSanin!
+
+ ### 🐞 Fixed
+
+ - AWS Security Hub integrations now persist successful recovery checks during finding delivery, keeping connection status and the last-checked time accurate.
+ - Social sign-up now creates authentication, tenant, and membership records in a single transaction, fully rolling back failed provisioning to prevent incomplete accounts.
+ - The SAML configuration form keeps the ACS URL field stable while generating the callback URL and exposes the copy action only after a valid URL is available.
+ - SAML users without a `userType` attribute and without an existing role now receive a least-privilege `read_only` fallback role, so role-dependent operations continue to work without granting management permissions.
+
+ ### 🔐 Security
+
+ - Provider deletion, connection checks, scan creation, provider secrets, provider groups, and daily schedules now respect role provider-group visibility.
+ - HTML reports escape provider-originated finding fields, preventing stored cross-site scripting through malicious cloud resource tags. https://github.com/prowler-cloud/prowler/security/advisories/GHSA-c2jg-2778-ggm4
+ - Authentication with an API key whose owning user was deleted now returns `401`, and user deletion revokes the user's API keys across all their tenants.
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @tomitobio: Huawei Cloud provider with CIS 1.0 benchmark ([#11950](https://github.com/prowler-cloud/prowler/pull/11950))
+ - @paramanandmallik: four AWS privilege-escalation Attack Paths queries ([#11460](https://github.com/prowler-cloud/prowler/pull/11460))
+ - @Sid-0602: AWS `codecommit` service and `codecommit_repository_no_secrets` check ([#11846](https://github.com/prowler-cloud/prowler/pull/11846))
+ - @l46983284-cpu, @Rishi943, and @UTKARSH698: AWS `glue_catalog_connection_no_secrets` check ([#11963](https://github.com/prowler-cloud/prowler/pull/11963))
+ - @Nithin078: AWS `ec2_instance_stopped_older_than_specific_days` ([#12076](https://github.com/prowler-cloud/prowler/pull/12076)) and `sagemaker_endpoint_config_kms_encryption_enabled` ([#12118](https://github.com/prowler-cloud/prowler/pull/12118), co-authored with @l46983284-cpu) checks
+ - @AlexanderSanin: MITRE ATT&CK enrichment in OCSF detection finding output ([#11492](https://github.com/prowler-cloud/prowler/pull/11492))
+ - @stefanobaldo: GCP gen2 Cloud Functions IAM policy retrieval is now thread-safe ([#12107](https://github.com/prowler-cloud/prowler/pull/12107))
+ - @rayair250-droid: GCP SSH and RDP firewall checks now detect exposed ports in any position within multi-port rules ([#12115](https://github.com/prowler-cloud/prowler/pull/12115))
+ - @jbchief-dev: secret ignore patterns now use Kingfisher-compatible LF line indexing ([#12141](https://github.com/prowler-cloud/prowler/pull/12141))
+ - @bmbferreira: Helm chart improvements — immutable chart versions on release ([#12056](https://github.com/prowler-cloud/prowler/pull/12056)) and capped Celery worker concurrency ([#12054](https://github.com/prowler-cloud/prowler/pull/12054))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.37.0) for the complete list of changes.
+
+
+
+ ### 🎫 Finding Groups - Jira
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Selected Findings, Finding Groups, and mixed selections can now be sent to Jira. When you select multiple findings, choose between one grouped issue or separate issues. Generated issues keep their Prowler context with deep links and filter details, while the UI provides clear dispatch and failure feedback.
+
+ 
+
+ Read more in the [Jira integration documentation](/user-guide/tutorials/prowler-app-jira-integration).
+
+ ### 🕸️ Attack Paths - Queries
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Prowler Cloud now records which built-in Attack Paths queries returned data at the end of each scan. The query selector hides confirmed-empty queries for the selected scan, so you can focus on paths that exist without opening blank graph views. Errored, unknown, and parameterized queries remain available when they still require investigation or input.
+
+ All Attack Paths queries are now published on [Prowler Hub](https://hub.prowler.com), where you can browse the full catalog.
+
+ 
+
+ Read more in the [Attack Paths documentation](/user-guide/tutorials/prowler-app-attack-paths).
+
+ ### 🧑🏫 New Tutorials: Connect Your AI Agents to Prowler Cloud
+
+
+ This feature needs a Prowler Cloud API key, so it is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ New tutorials walk you through connecting your own AI agents to Prowler Cloud, so they can query your security posture and act on it programmatically.
+
+ Read more in the [AI agents documentation](/user-guide/ai-agents/index).
+
+ ### ☁️ Region-less Oracle Cloud Infrastructure Setup
+
+ Oracle Cloud Infrastructure (OCI) provider credentials no longer require a region. Existing clients can still send the legacy `region` field for compatibility, but the API ignores it before storing credentials or starting a scan. This removes an unnecessary step from OCI onboarding.
+
+ Read more in the [OCI documentation](/user-guide/providers/oci/getting-started-oci).
+
+ ### 🔍 Checks
+
+ #### AWS
+
+ - `sagemaker_notebook_instance_no_secrets` scans the `OnCreate` and `OnStart` lifecycle scripts of SageMaker notebook instances for hardcoded API keys, passwords, tokens, connection strings, and other secrets. Thanks to @kiranrajsg!
+
+ Read more in the [AWS documentation](/user-guide/providers/aws/getting-started-aws). Explore all AWS checks at [Prowler Hub](https://hub.prowler.com/check?provider=aws).
+
+ ### 🔐 Security
+
+ - Integration responses and operations now respect provider visibility, preventing hidden-provider disclosure and blocking unauthorized attachment, connection checks, Jira dispatches, edits, and deletion.
+ - Next.js was updated from 16.2.9 to 16.2.11, patching four high-severity and five medium-severity vulnerabilities.
+ - The unused `npm` CLI was removed from the UI container image, eliminating the bundled `node-tar` CVE-2026-59873 and reducing exposure to future bundled npm vulnerabilities.
+ - Vitest and its browser packages were updated from 4.1.8 to 4.1.10, resolving the critical `@vitest/browser` file-access permission bypass. These are development dependencies and have no runtime impact.
+ - Kubernetes kubeconfig validation now blocks legacy `auth-provider.config.cmd-path` command authentication, closing a command-execution bypass.
+ - `next-auth` was updated from 5.0.0-beta.30 to 5.0.0-beta.32, patching two critical Auth.js advisories: existence-based authorization checks that could fail open when a provider is misconfigured, and a homoglyph `@` bypass in email address normalization. The bump also pulls in the patched `@auth/core` 0.41.3 transitively.
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @kiranrajsg: AWS `sagemaker_notebook_instance_no_secrets` check ([#11843](https://github.com/prowler-cloud/prowler/pull/11843))
+ - @owenchenxy: Alibaba Cloud SSH and RDP security group checks now handle capitalized `Policy="Accept"` values correctly ([#12049](https://github.com/prowler-cloud/prowler/pull/12049))
+ - @rsaladra: S3 bucket name validation no longer raises an invalid escape sequence `SyntaxWarning` at startup ([#12041](https://github.com/prowler-cloud/prowler/pull/12041))
+ - @SujayKulkarni-2211: Updated the AWS check count in the README ([#12011](https://github.com/prowler-cloud/prowler/pull/12011))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.36.0) for the complete list of changes.
+
+
+
+ ### 💬 Lighthouse AI - Side Chat
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Lighthouse AI now lives in a side panel you can open from anywhere in the app. Ask about the findings you are looking at without leaving the page, and expand to the full-page chat at any time: your draft, messages, and streaming response come along. Finding and resource details share the same panel, with tabs to switch between Details and Lighthouse AI.
+
+ 
+
+ Read more in the [Lighthouse AI documentation](/getting-started/products/prowler-cloud-lighthouse#side-panel).
+
+ ### 🤖 Lighthouse AI - Take Action
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Lighthouse AI is no longer read-only. Ask it to do things and it will: connect or remove providers, trigger a scan, schedule daily scans, update scan settings, and manage your mutelist and mute rules, straight from the chat. Every action is gated by RBAC: Lighthouse can only do what the user asking could do themselves.
+
+ Read more in the [Lighthouse AI capabilities](/getting-started/products/prowler-cloud-lighthouse#capabilities).
+
+ ### ☁️ One-step AWS Organizations onboarding
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Onboarding an entire AWS Organization is now a single step. One CloudFormation quick-create link deploys the management account role and a service-managed StackSet that rolls the role out to every member account, replacing the manual StackSet console setup. Target the whole organization or a specific Organizational Unit or Root ID, and deploy from the management account or a delegated administrator. The S3 integration quick-create link also pre-fills the bucket owner account ID, preventing a stack validation error.
+
+ 
+
+ Built on the full-organization CloudFormation template contributed by @jchrisfarris — thanks!
+
+ Read more in the [AWS Organizations documentation](/user-guide/tutorials/prowler-cloud-aws-organizations).
+
+ ### 🎯 Scan configurations: exclude checks and services
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Scan configurations now accept `excluded_checks` and `excluded_services` to narrow the execution scope. Skip individual checks or entire services per provider, and the scan does not run them at all: less noise, faster scans, and no findings you would mute anyway.
+
+ Read more in the [Scan Configuration documentation](/user-guide/tutorials/prowler-app-scan-configuration#limiting-the-scan-scope).
+
+ ### 🧭 Redesigned sidebar navigation
+
+ The sidebar was redesigned around how you actually work: grouped sections for security, settings, and help, a Home/Chat switch at the top, collapsible configuration entries, clearer active states, and a responsive mobile overlay.
+
+ 
+
+ ### 🔌 Prowler MCP tools renamed to `prowler_*`
+
+ Core Prowler tools in Prowler MCP moved from the `prowler_app_*` prefix to the shorter `prowler_*` namespace, and the MCP documentation was restructured around it. Legacy `prowler_app_*` names keep working in Lighthouse AI, so existing setups are not broken.
+
+ Read more in the [Prowler MCP tools reference](/getting-started/basic-usage/prowler-mcp-tools).
+
+ ### 🔐 Security
+
+ - Jira integration credentials now only accept bare Atlassian site names (letters, numbers, and hyphens), and Jira tenant information requests validate site names and no longer follow redirects.
+ - Social account linking now requires a verified matching email from both the identity provider and the existing user account, and account connection notification emails are disabled.
+ - 13 advisories reported by `pnpm audit` on the UI (3 high, 9 moderate, 1 low) are resolved with patched versions of `hono`, `ws`, `vite`, `dompurify`, `js-yaml`, `@opentelemetry/core`, and `@babel/core`, including `hono` CVE-2026-59896.
+
+ ### 🙌 External Contributors
+
+ No external contributors in this release.
+
+ Special mention to @jchrisfarris, whose full-organization CloudFormation template from v5.34.0 powers the new one-step AWS Organizations onboarding ([#10403](https://github.com/prowler-cloud/prowler/pull/10403)).
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.35.0) for the complete list of changes.
+
+
+
+ ### 🏷️ New product names
+
+ The Prowler family has grown, and the names now say what each product is. Same products, clearer names:
+
+ **Prowler products:**
+
+ - **Prowler Cloud** — the managed cloud security platform operated by the Prowler team.
+ - **Prowler Private Cloud** (formerly *Prowler Enterprise*) — the self-hosted deployment of Prowler Cloud in your own environment.
+ - **Prowler Hub** — the free public library of versioned checks, cloud service artifacts, and compliance frameworks.
+ - **Prowler Lighthouse AI** — The Agentic Cloud Defender in Prowler Cloud and Prowler Private Cloud.
+ - **Prowler MCP** — the MCP server that connects AI assistants and agents to Prowler, including the IDE plugins.
+
+ **Open source projects:**
+
+ - **Prowler CLI** — the command-line scanner for all supported providers.
+ - **Prowler Local Server** (formerly *Prowler App*) — the self-hosted web application and API to run scans, visualize findings, and manage providers.
+ - **Prowler Local Dashboard** — the web dashboard for visualizing Prowler CLI scan results, distributed with the CLI.
+ - **Prowler SDK** — the Python library behind Prowler CLI and Prowler Local Server.
+
+ See the full family in the [Prowler products documentation](/getting-started/products).
+
+ ### 🧭 Cross-Provider Compliance
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ One framework, every cloud, a single answer. The new **Cross-provider** tab in Compliance takes the most recent completed scan of every compatible provider and rolls them up into a single compliance posture per framework, with a per-provider breakdown and a combined executive PDF report. Requirement status follows strict precedence (FAIL over PASS over MANUAL), so one failing provider is enough to flag a requirement across your whole estate.
+
+ 
+
+ Three universal frameworks support it today:
+
+ - **CIS Controls 8.1** — AWS, Azure, Google Cloud, Microsoft 365, Kubernetes, GitHub, Google Workspace, Okta, Oracle Cloud, Alibaba Cloud, Cloudflare, MongoDB Atlas, OpenStack, and Vercel.
+ - **CSA CCM 4.0** — AWS, Azure, Google Cloud, Alibaba Cloud, and Oracle Cloud.
+ - **DORA 2022/2554** — AWS, Azure, Google Cloud, Alibaba Cloud, and Cloudflare.
+
+ Filter by provider type, account, or provider group, drill into each framework's requirements, and export the combined PDF.
+
+ 
+
+ Read more in the [Cross-Provider Compliance documentation](/user-guide/compliance/tutorials/cross-provider-compliance).
+
+ ### 🏢 New Provider — E2E Networks
+
+ Prowler now scans [**E2E Networks**](https://www.e2enetworks.com/), with **27 checks** spanning compute nodes, networking, security groups, load balancers, block and file storage, and managed databases. Thanks to @deepak7093 for their 1st provider in Prowler!
+
+ Available in the Prowler CLI:
+
+ ```bash
+ export E2E_NETWORKS_API_KEY="your-api-key"
+ export E2E_NETWORKS_AUTH_TOKEN="your-auth-token"
+ export E2E_NETWORKS_PROJECT_ID="your-project-id"
+ prowler e2enetworks
+ ```
+
+ Read more in the [E2E Networks documentation](/user-guide/providers/e2enetworks/getting-started-e2enetworks). Explore all E2E Networks checks at [Prowler Hub](https://hub.prowler.com/check?provider=e2enetworks).
+
+ ### 🔐 Security
+
+ User role relationship updates in the API are now limited to the active tenant, preserving the role assignments the same user holds in other tenants.
+
+ ### 🔍 Checks
+
+ #### AWS
+
+ - `ec2_ami_account_block_public_access` — verifies AMI block public access is enabled at the account level in each Region, so AMIs cannot be shared publicly. Thanks to @goutham-hari!
+ - `datapipeline_pipeline_no_secrets_in_definition` — scans Data Pipeline object fields, parameter objects, and parameter values for hardcoded secrets with Kingfisher. Thanks to @YinkaMetrics!
+ - `elbv2_listener_pqc_tls_enabled` — verifies ELBv2 HTTPS/TLS listeners use post-quantum TLS security policies with TLS 1.2 or higher, helping reduce harvest-now-decrypt-later exposure.
+ - `amplify_app_no_secrets_in_environment` — scans Amplify app and branch environment variables and build settings (buildSpec) for hardcoded secrets with Kingfisher. Thanks to @Deep070203!
+
+ #### Azure
+
+ - `app_function_ensure_http_is_redirected_to_https` — verifies that Function Apps enforce HTTPS-only traffic. Thanks to @amandalal007!
+
+ #### Kubernetes
+
+ - `core_minimize_hostpath_volume_mounts` — detects Pods that use `hostPath` volumes. Thanks to @0xTaoZ!
+ - `core_readonly_root_filesystem_enabled` — verifies that every container in each Pod explicitly sets `readOnlyRootFilesystem: true` in its security context. Thanks to @Weedle02!
+
+ #### STACKIT
+
+ - `iaas_server_public_ip_attached` — flags IaaS servers that have a public IP address directly attached to a network interface. Thanks to @johannes-engler-mw!
+
+ Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @jchrisfarris — Deploy AWS Organizations with the CloudFormation template in one step ([#10403](https://github.com/prowler-cloud/prowler/pull/10403))
+ - @deepak7093 — New E2E Networks provider: 27 checks across compute nodes, networking, security groups, load balancers, block/file storage, and managed databases ([#11654](https://github.com/prowler-cloud/prowler/pull/11654))
+ - @goutham-hari — AWS `ec2_ami_account_block_public_access` check ([#11828](https://github.com/prowler-cloud/prowler/pull/11828))
+ - @YinkaMetrics — AWS `datapipeline_pipeline_no_secrets_in_definition` check ([#11821](https://github.com/prowler-cloud/prowler/pull/11821))
+ - @amandalal007 — Azure `app_function_ensure_http_is_redirected_to_https` check ([#11929](https://github.com/prowler-cloud/prowler/pull/11929))
+ - @0xTaoZ — Kubernetes `core_minimize_hostpath_volume_mounts` check ([#11837](https://github.com/prowler-cloud/prowler/pull/11837))
+ - @Weedle02 — Kubernetes `core_readonly_root_filesystem_enabled` check ([#11835](https://github.com/prowler-cloud/prowler/pull/11835))
+ - @johannes-engler-mw — STACKIT `iaas_server_public_ip_attached` check ([#11549](https://github.com/prowler-cloud/prowler/pull/11549))
+ - @janderik — Trailing newlines added to compliance, region, and fixture data files for POSIX compliance ([#11765](https://github.com/prowler-cloud/prowler/pull/11765))
+ - @Deep070203 — AWS `amplify_app_no_secrets_in_environment` check ([#11825](https://github.com/prowler-cloud/prowler/pull/11825))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.34.0) for the complete list of changes.
+
+
+
+ ### 🤖 Lighthouse AI — The Agentic Cloud Defender
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Lighthouse AI is now a full agentic assistant wired to the Prowler Cloud backend. Ask it about your findings, your compliance posture, or your riskiest resources, and watch it work: the agent discovers and runs the Prowler tools it needs to answer, with every tool call visible in the new agentic view. It reads your security data through read-only tools, so it can never touch secrets or modify your tenant.
+
+ 
+
+ The chat experience is rebuilt around **persistent sessions**: conversations stream in real time, stay in your session history, can be archived, and a **sidebar chat mode** lets you ask questions from any page in the app without losing your place.
+
+ 
+
+ You control the brain behind it. Configure one or more LLM providers — **OpenAI**, **Amazon Bedrock**, or any **OpenAI-compatible** endpoint (OpenRouter, Ollama) — with connection testing built into the setup and per-provider model selection. Add a shared **business context** (your security goals, compliance needs, organizational priorities) and every session uses it to give answers that fit your environment.
+
+ 
+
+ Read more in the [Lighthouse AI documentation](/getting-started/products/prowler-cloud-lighthouse) and the [multiple LLM providers guide](/user-guide/tutorials/prowler-cloud-lighthouse-multi-llm).
+
+ ### 📄 Compliance PDF Reports Without Credentials
+
+ Compliance PDF reports no longer require the provider's credentials to be present. Findings are now enriched from the provider metadata stored in the database, so a report still generates even after the provider secret has been deleted or its credentials have become invalid.
+
+ Read more in the [compliance documentation](/user-guide/compliance/tutorials/compliance).
+
+ ### ⏳ Scan Queueing
+
+ Overlapping scans for the same provider now queue behind the active one instead of dispatching concurrent scan workers. Launch a manual scan while a scheduled one is running and it waits its turn. No more duplicated work or racing scans.
+
+ ### 🔐 Security
+
+ The Kubernetes provider credentials now reject kubeconfigs using `exec` authentication in Prowler Cloud, at the API and in the credential form, preventing user-supplied commands from running on Cloud workers.
+
+ Read more in the [Kubernetes provider authentication documentation](/user-guide/providers/kubernetes/getting-started-k8s#step-2-configure-kubernetes-authentication).
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @kratos0718 — Azure `postgresql_flexible_server_log_retention_days_greater_3` Flexible Server log retention fix ([#11761](https://github.com/prowler-cloud/prowler/pull/11761))
+ - @Sanjays2402 — `KeyError: 'MANUAL'` crash fix in the compliance summary table, shipped early in v5.32.1 ([#11823](https://github.com/prowler-cloud/prowler/pull/11823))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.33.0) for the complete list of changes.
+
+
+
+ ### 🔎 Findings Triage
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Triage findings straight from the Findings view. Each finding gets a triage status you can move through its lifecycle:
+
+ **Open → Under Review → Remediating → Risk Accepted → False Positive → Resolved**
+
+ Add a triage note to record the decision, mute a finding, all from the row's actions menu. The current status shows inline on every finding row, so you keep track of what has been reviewed and stop re-checking the same issues scan after scan.
+
+ 
+
+ The status also follows the finding automatically across scans: when a finding flips from `FAIL` to `PASS` on the next scan it moves to **Resolved**, and when it flips from `PASS` back to `FAIL` it moves to **Reopened**. You always know whether an issue is genuinely fixed or has regressed, without touching it by hand.
+
+ 
+
+ Read more in the [Findings Triage documentation](/user-guide/tutorials/prowler-app-findings-triage).
+
+ ### ⚙️ Scan Configuration
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Create named, reusable scan configurations from a dedicated **Scans / Configuration** page. Each configuration is YAML that follows the structure of [`prowler/config/config.yaml`](https://github.com/prowler-cloud/prowler/blob/master/prowler/config/config.yaml), so you only include the keys you want to override; the rest fall back to the built-in defaults. Values are validated on save against a per-provider, type-safe configuration schema that range-checks each field and rejects unknown keys, so a malformed config is caught before it ever reaches a scan. Attach a configuration to one or more providers so it applies on their next scan, or save it now and attach providers later.
+
+ 
+
+ From the Providers view you can pick which configuration a provider uses (`Default` or any of your saved ones) without leaving the page. No more passing config files around by hand.
+
+ 
+
+ Read more in the [Scan Configuration documentation](/user-guide/tutorials/prowler-app-scan-configuration).
+
+ ### ✅ Per-Requirement Configuration Validation
+
+
+ This feature is available exclusively in **Prowler Cloud** and **Prowler Private Cloud** with a [subscription](https://prowler.com/pricing).
+
+
+ Compliance frameworks can now declare `ConfigRequirements` on a requirement, so it's reported as **FAIL** when its mapped checks ran under a configuration too loose to satisfy it. Even if every individual finding PASSed. This applies across all compliance outputs: CSV, OCSF, and console tables, and is the engine behind Scan Configuration's "marked as FAIL" behavior described above.
+
+ 
+
+ Read more in the [Configuration File documentation](/user-guide/cli/tutorials/configuration_file).
+
+ ### ⏱️ Okta — Request Throttling & Retries
+
+ Prowler now proactively throttles Okta API requests to stay under rate limits, with reactive retries on HTTP 429 as a safety net. Both are set in the scan configuration (or their equivalent CLI flags):
+
+ - `okta_requests_per_second` (config file) / `--okta-requests-per-second` (CLI) — cap the request rate. Default: 4 req/s.
+ - `okta_max_retries` (config file) / `--okta-retries-max-attempts` (CLI) — bound retry attempts. Default: 5.
+
+ This makes large Okta scans more reliable and less likely to be rate-limited.
+
+ Read more in the [Okta rate limit documentation](/user-guide/providers/okta/retry-configuration#request-throttling-requests-per-second).
+
+ ### 📉 AWS — Cap Resources Scanned per Service
+
+ Large AWS accounts can now cap how many resources Prowler analyzes for the highest-volume services, keeping scan time and cost under control. Set a global limit with `max_scanned_resources_per_service`, or override it per service:
+
+ - EBS snapshots (`max_ebs_snapshots`)
+ - Backup recovery points (`max_backup_recovery_points`)
+ - CloudWatch log groups (`max_cloudwatch_log_groups`)
+ - Lambda functions (`max_lambda_functions`)
+ - ECS task definitions (`max_ecs_task_definitions`)
+ - CodeArtifact packages (`max_codeartifact_packages`)
+
+ Limits are **disabled by default** (`0` = unlimited); only positive values cap the analyzed resources.
+
+
+ When a positive limit is set, compliance results reflect only the sampled resources, not every matching resource in the account.
+
+
+ Read more in the [configuration file documentation](/user-guide/cli/tutorials/configuration_file#supported-aws-resource-limits).
+
+ ### 🏷️ Azure — Filter by Resource Group
+
+ Azure scans can now be scoped to one or more resource groups with the new `--azure-resource-group` / `--azure-resource-groups` option. This lets you run focused assessments against specific environments, teams, or workloads instead of scanning every accessible resource in the subscription. Thanks to @Legin-ML for contributing this feature!
+
+ ```bash
+ # Single resource group
+ prowler azure --az-cli-auth --azure-resource-group rg-prod
+
+ # Multiple resource groups
+ prowler azure --az-cli-auth --azure-resource-group rg-prod1 rg-prod2
+ ```
+
+ Read more in the [Azure Resource Groups documentation](/user-guide/providers/azure/resource-groups).
+
+ ### 🧭 Provider Group Filter
+
+ Filter the **Overview, Findings, Resources, Scans, and Providers** views by provider group. Scope the whole app to a team, an environment, or a business unit in one click instead of filtering provider by provider.
+
+ 
+
+ Read more about managing provider groups in the [RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
+
+ ### 🔬 API — Timestamp Precision in Findings Filters
+
+ The `/api/v1/findings` endpoint now accepts full timestamps on the `inserted_at` and `updated_at` filters (`filter[inserted_at__gte]`, `filter[inserted_at__lte]`, and the `updated_at` variants), so you can query narrow time windows instead of whole days. Date-only filtering keeps working, so existing integrations are unaffected.
+
+ ```bash
+ # Findings inserted within a precise timestamp window
+ curl --globoff \
+ 'http://localhost:8080/api/v1/findings?filter[inserted_at__gte]=2026-07-01T06:12:18Z&filter[inserted_at__lte]=2026-07-02T19:25:55Z' \
+ -H 'Authorization: Bearer ' \
+ -H 'Accept: application/vnd.api+json'
+ ```
+
+ ### 🕸️ Attack Paths — Neptune as a persistent sink
+
+ Attack Paths can now persist its graph in **AWS Neptune** in addition to Neo4j, selectable via `ATTACK_PATHS_SINK_DATABASE=neptune` (default `neo4j`). Cartography's per-scan ingest database stays on Neo4j. The scan task preflights the ingest database and the configured sink before ingestion, and provider graph cleanup now deletes relationships in directed batches before deleting nodes.
+
+ This is the groundwork for scale: a managed graph database lets Attack Paths hold much larger graphs, extend coverage to more providers, and link resources across them so an attack path can cross provider boundaries instead of stopping at one cloud's edge.
+
+ Read more in the [Attack Paths documentation](/user-guide/tutorials/prowler-app-attack-paths).
+
+ ### 🔐 New Secret-Scanning Engine — Kingfisher
+
+ Prowler's secret-scanning checks now run on [Kingfisher](https://github.com/mongodb/kingfisher) instead of `detect-secrets`. Scans run **fully offline by default**, and obvious placeholder values (e.g. `password123`, `changeme`) are no longer reported, cutting down false positives.
+
+ Opt in to **live validation** with the new `--scan-secrets-validate` flag (or the `aws.secrets_validate` config option): Prowler checks discovered secrets against the provider APIs, and any secret confirmed to be **live is reported as critical**, so you can prioritize the credentials that actually work.
+
+
+ The `detect_secrets_plugins` configuration option has been removed, as it is no longer used by the new engine.
+
+
+ Read more in the [secret detection documentation](/user-guide/cli/tutorials/pentesting#detect-secrets).
+
+ ### 🔍 Checks
+
+ #### AWS
+
+ - `stepfunctions_statemachine_encrypted_with_cmk` — Step Functions state machines use a customer-managed KMS key for encryption at rest instead of the default AWS-owned key. Thanks to @Sid-0602!
+ - `waf_regional_webacl_logging_enabled` — AWS WAF Classic Regional Web ACLs have logging enabled to a Kinesis Data Firehose stream. Thanks to @Sid-0602!
+ - **IAM privilege escalation** — the privesc checks now cover **AWS Bedrock AgentCore** paths across Runtime, Harness, Code Interpreter, and Custom Browser. Thanks to @MrCloudSec!
+ - `apigateway_restapi_no_secrets_in_stage_variables` — scans API Gateway REST API stage variables for hardcoded passwords, API keys, and tokens. Thanks to @chirag1206!
+ - `awslambda_function_no_secrets_in_code` — this check now supports a `secrets_ignore_files` audit-config option to skip files inside the deployment package by glob pattern (e.g. `*.deps.json`), suppressing .NET dependency-manifest false positives without masking real secrets.
+ - `s3_bucket_object_public` — spot-checks a configurable sample of object ACLs in each bucket and flags objects granted to the `AllUsers` or `AuthenticatedUsers` groups. Disabled by default; opt in via the `s3_bucket_object_public_enabled` configuration option. Thanks to @Synchx00!
+
+ #### Microsoft 365
+
+ New **Conditional Access** hardening checks:
+
+ - `entra_conditional_access_policy_explicitly_targets_azure_devops` — at least one enabled policy explicitly includes the Azure DevOps cloud application, rather than relying on a broad "All cloud apps" policy. Thanks to @mzl2233!
+ - `entra_conditional_access_policy_no_exclusion_gaps` — every user, group, role, or application excluded from an enabled policy stays in scope of another enabled policy. Thanks to @UTKARSH698 with @arieleli01212 as co-author!
+ - `entra_conditional_access_policy_groups_management_restricted` — every security group referenced by an enabled or report-only policy is management-restricted or role-assignable. Thanks to @SAMurai-16!
+ - `exchange_application_access_policy_restricts_mailbox_apps` — every service principal with Microsoft Graph application-level Exchange mailbox permissions is restricted by an Exchange Online Application Access Policy. Thanks to @VasistAcharya!
+
+ ### 📚 Compliance
+
+ #### CIS Benchmark Refresh — Six New Versions
+
+ Prowler ships a coordinated refresh of the CIS Benchmarks across six providers:
+
+ - **AWS** — CIS Amazon Web Services Foundations Benchmark v7.0.0, adding the new Organizations section (2.1.1-2.1.6), resource policy (2.21), web front-end access logging (4.10), and VPC Endpoints (6.8) recommendations.
+ - **Azure** — CIS Microsoft Azure Foundations Benchmark v6.0.0.
+ - **GCP** — CIS Google Cloud Platform Foundation Benchmark v5.0.0.
+ - **Kubernetes** — CIS Kubernetes Benchmark v2.0.1.
+ - **GitHub** — CIS GitHub Benchmark v1.2.0.
+ - **Microsoft 365** — CIS Microsoft 365 Foundations Benchmark v7.0.0.
+
+ #### CIS Controls v8.1 — Universal Framework
+
+ A new **universal** (cross-provider) compliance framework mapping existing checks across 18 providers — AWS, Azure, GCP, Kubernetes, M365, GitHub, AlibabaCloud, OracleCloud, GoogleWorkspace, Okta, Cloudflare, Vercel, MongoDB Atlas, OpenStack, Linode, StackIT, NHN, and Scaleway — to the 18 CIS Critical Security Controls and their Safeguards. Ships with a dedicated detail view and report mapping in the UI.
+
+ Read more in the [compliance documentation](/user-guide/compliance/tutorials/compliance). Explore the full compliance catalog at [Prowler Hub](https://hub.prowler.com/compliance).
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @chirag1206 — `apigateway_restapi_no_secrets_in_stage_variables` check ([#11188](https://github.com/prowler-cloud/prowler/pull/11188))
+ - @MrCloudSec — AWS Bedrock AgentCore privilege escalation paths in the IAM privesc checks ([#11726](https://github.com/prowler-cloud/prowler/pull/11726))
+ - @Sid-0602 — `stepfunctions_statemachine_encrypted_with_cmk` ([#11538](https://github.com/prowler-cloud/prowler/pull/11538)) and `waf_regional_webacl_logging_enabled` ([#11539](https://github.com/prowler-cloud/prowler/pull/11539)) checks
+ - @mzl2233 — `entra_conditional_access_policy_explicitly_targets_azure_devops` check ([#11182](https://github.com/prowler-cloud/prowler/pull/11182))
+ - @UTKARSH698 with @arieleli01212 as co-author — `entra_conditional_access_policy_no_exclusion_gaps` check ([#11577](https://github.com/prowler-cloud/prowler/pull/11577))
+ - @SAMurai-16 — `entra_conditional_access_policy_groups_management_restricted` check ([#11342](https://github.com/prowler-cloud/prowler/pull/11342))
+ - @vahidg — Azure PostgreSQL flexible server collection resilience fix ([#11595](https://github.com/prowler-cloud/prowler/pull/11595))
+ - @davletd — Azure `keyvault_logging_enabled` `AuditEvent` category fix ([#11660](https://github.com/prowler-cloud/prowler/pull/11660))
+ - @VasistAcharya — `exchange_application_access_policy_restricts_mailbox_apps` ([#11247](https://github.com/prowler-cloud/prowler/pull/11247))
+ - @Legin-ML — Filter scans at Resource Group level ([#10657](https://github.com/prowler-cloud/prowler/pull/10657))
+ - @Synchx00 — `s3_bucket_object_public` check ([#9517](https://github.com/prowler-cloud/prowler/pull/9517))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.32.0) for the complete list of changes.
+
+
+
+ ### 🗓️ Flexible Scan Scheduling
+
+
+ Available exclusively in **Prowler Cloud**. Prowler Local Server supports daily scans only.
+
+
+ 
+
+ You can now set a per-provider scan schedule from the Providers page. Pick a **scan time** and a **repeat cadence**: Daily, Every 48 hours, Weekly (with a day-of-week selector), or Monthly. Schedules can be edited or removed at any time, and a new scan never interrupts access to existing data.
+
+ 
+
+ All schedules are listed in one place under the **Scheduled** tab in **Scan Jobs**, showing each provider's cadence, next scan, and last scan at a glance.
+
+ 
+
+ Read more in the [scan scheduling documentation](/user-guide/tutorials/prowler-scan-scheduling).
+
+ ### 📚 DORA — Expanded Provider Coverage
+
+ Prowler extends [**DORA**](https://www.eiopa.europa.eu/digital-operational-resilience-act-dora_en) (Digital Operational Resilience Act, Regulation (EU) 2022/2554) coverage to **Azure**, **GCP**, **Cloudflare**, and **Alibaba Cloud**, mapping each provider's existing checks across the five DORA pillars.
+
+ 
+
+
+ The framework follows the `_` naming convention as `DORA_2022_2554`.
+
+
+ Read more in the [compliance documentation](/user-guide/compliance/tutorials/compliance).
+
+ ### 🚀 Guided Onboarding
+
+
+ Available exclusively in **Prowler Cloud**.
+
+
+ New accounts now get a guided first-run experience. The Overview greets you with an **"Add your first provider"** prompt: connect a provider so Prowler has something to scan and assess, then get started in one click (or skip for now).
+
+ 
+
+ From there, contextual empty states across the product point you to the next action rather than leaving you stuck. Attack Paths, for example, explains that you need a completed scan before it can build a graph and links straight to **Scan Jobs**, with a **"See how it works"** affordance for first-timers.
+
+ 
+
+ ### 🔐 Optional SAML SSO `userType`
+
+ The SAML `userType` attribute is now optional. If your IdP does not send it, or sends it blank, Prowler keeps the user's existing roles unchanged instead of replacing them with a fallback role.
+
+ When `userType` is provided, Prowler still maps the user to the matching role. If that role does not exist yet, Prowler creates it with read-only access: visibility over all providers, with no management permissions.
+
+ Read more in the [SAML SSO documentation](/user-guide/tutorials/prowler-app-sso).
+
+ ### 🏢 New Provider — Linode
+
+ Prowler now scans [**Linode**](https://www.linode.com/) (Akamai Cloud), covering its administration, compute, and networking services. Thanks to @varunmamillapalli for their 1st provider in Prowler!
+
+
+ Linode is not officially supported. For more information, [contact us](https://prowler.com/contact).
+
+
+ Read more in the [Linode documentation](/user-guide/providers/linode/getting-started-linode). Explore all Linode checks at [Prowler Hub](https://hub.prowler.com/check?provider=linode).
+
+ ### 🔍 Checks
+
+ #### AWS
+
+ **Post-Quantum Cryptography readiness** — get ahead of the migration to quantum-resistant cryptography:
+
+ - `cloudfront_distributions_pqc_tls_enabled` — CloudFront distributions enforce a post-quantum TLS 1.3 security policy.
+ - `apigateway_domain_name_pqc_tls_enabled` — API Gateway custom domain names use a post-quantum TLS security policy.
+ - `transfer_server_pqc_ssh_kex_enabled` — Transfer Family servers use a post-quantum hybrid SSH key exchange.
+ - `acmpca_certificate_authority_pqc_key_algorithm` — Private CA authorities use a post-quantum (ML-DSA) key algorithm (new `acmpca` service).
+ - `rolesanywhere_trust_anchor_pqc_pki` — IAM Roles Anywhere trust anchors are backed by a post-quantum (ML-DSA) PKI (new `rolesanywhere` service).
+
+ **Organization-wide governance:**
+
+ - `securityhub_delegated_admin_enabled_all_regions` — Security Hub has a delegated administrator, active in all opted-in regions, with organization auto-enable on. Thanks to @ernestprovo23!
+ - `config_delegated_admin_and_org_aggregator_all_regions` — AWS Config has a delegated administrator and an organization aggregator covering all regions. Thanks to @ernestprovo23!
+
+ **Machine learning:**
+
+ - `sagemaker_clarify_exists` — verifies at least one SageMaker Clarify processing job exists per scanned region, so bias-detection and model-explainability controls are in place. Thanks to @AlexanderSanin!
+
+ #### Azure
+
+ A large batch of new Azure checks spanning data, compute, identity, and networking:
+
+ - **Cosmos DB** — automatic failover, continuous backup policy, minimum TLS 1.2, and public network access disabled.
+ - **MySQL & PostgreSQL Flexible Servers** — geo-redundant backup and high availability.
+ - **AKS** — auto-upgrade, Azure Monitor (Container Insights), local accounts disabled, and Microsoft Defender enabled.
+ - **Databricks** — public network access disabled and secure cluster connectivity (no public IP).
+ - **Defender** — CSPM on the Standard tier.
+ - **Networking** — NSG association on subnets and DDoS Network Protection on VNets.
+ - **Entra ID** — app registration credential expiry, users with recent sign-in and strong authentication enforcement.
+ - **Recovery Services** — vaults with at least one protected backup item and vaults with adequate backup policy.
+
+ Thanks to @s1ns3nz0 for all these contributions!
+
+ #### GCP
+
+ New coverage for high availability and public-exposure detection:
+
+ - `cloudsql_instance_high_availability_enabled` — Cloud SQL primary instances use `REGIONAL` availability for automatic zone failover.
+ - `cloudfunction_function_inside_vpc` — Cloud Functions use a Serverless VPC Access connector for private egress.
+ - `cloudfunction_function_not_publicly_accessible` — detects `allUsers` / `allAuthenticatedUsers` IAM invocation bindings.
+ - `secretmanager_secret_not_publicly_accessible` — detects Secret Manager secrets with public IAM bindings.
+ - `secretmanager_secret_rotation_enabled` — verifies Secret Manager secrets have automatic rotation configured with a period of 90 days or less and no missed rotation.
+
+ Thanks to @s1ns3nz0 for all these contributions!
+
+ #### Kubernetes
+
+ New core checks for container resource governance and reliability: CPU limits, CPU requests, memory limits, memory requests, fixed image tags, liveness probes, and readiness probes. Thanks to @Nikhilkumar2311 for all these contributions!
+
+ #### Microsoft 365
+
+ - `entra_directory_sync_object_takeover_blocked` — hybrid Entra tenants block cloud object takeover through soft-match and hard-match directory synchronization. Thanks to @PrettyFox0 and @omobolajiadeyan!
+ - `entra_conditional_access_policy_no_deleted_object_references` — flags Conditional Access policies that reference user, group, or role objects that no longer resolve in the directory. Thanks to @ernestprovo23!
+
+ #### Oracle Cloud Infrastructure
+
+ - `identity_storage_service_level_admins_scoped` — CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions.
+
+ Explore all checks at [Prowler Hub](https://hub.prowler.com/check).
+
+ ### 🐍 Python 3.13 Support
+
+ The Prowler SDK now supports **Python 3.13**. Thanks to @branchv!
+
+ ### 🔐 Security Updates
+
+ - **SDK** — `pytest` 8.3.5 → 9.0.3, `black` 25.1.0 → 26.3.1, `microsoft-kiota-*` → 1.9.9, and `aiohttp` → 3.14.0, patching known CVEs.
+ - **API** — `aiohttp` → 3.14.0 and `idna` → 3.15, patching known CVEs.
+ - **UI** — bumped vulnerable `Next.js`, React, AI SDK, `postcss`, `hono`, `qs`, `esbuild`, and Alpine OpenSSL packages; `dompurify` 3.4.2 → 3.4.10, patching XSS sanitization bypass advisories.
+ - **Containers** — base image bumped to `python:3.12.13-slim-bookworm` (patches `libgnutls30` CVE-2026-33845 and CVE-2026-42010) and `trivy` to 0.71.0 (patches embedded `golang.org/x/crypto` and Go stdlib CVEs).
+
+ ### 🙌 External Contributors
+
+ Thank you to our community contributors for this release!
+
+ - @varunmamillapalli — New Linode provider: administration, compute, and networking services ([#11633](https://github.com/prowler-cloud/prowler/pull/11633))
+ - @s1ns3nz0 — 20+ Azure & GCP checks across Cosmos DB, AKS, Databricks, Flexible Servers, Entra, networking, and GCP public-exposure
+ - @Nikhilkumar2311 — Kubernetes resource limits, requests, image tag, and probe checks ([#11373](https://github.com/prowler-cloud/prowler/pull/11373))
+ - @ernestprovo23 — AWS Security Hub/Config org-wide delegated admin checks ([#11259](https://github.com/prowler-cloud/prowler/pull/11259)) and M365 conditional access check ([#11236](https://github.com/prowler-cloud/prowler/pull/11236))
+ - @AlexanderSanin — `sagemaker_clarify_exists` check ([#11211](https://github.com/prowler-cloud/prowler/pull/11211))
+ - @PrettyFox0 with @omobolajiadeyan as co-author — M365 directory sync object takeover check ([#11098](https://github.com/prowler-cloud/prowler/pull/11098))
+ - @branchv — Python 3.13 support ([#9293](https://github.com/prowler-cloud/prowler/pull/9293))
+ - @alinealfa — GCP audit-filtered aggregated sinks fix ([#11575](https://github.com/prowler-cloud/prowler/pull/11575))
+ - @b-abderrahmane — Configurable Celery worker concurrency ([#11075](https://github.com/prowler-cloud/prowler/pull/11075))
+
+ See the [full release notes on GitHub](https://github.com/prowler-cloud/prowler/releases/tag/5.31.0) for the complete list of changes.
+
+
+
+ Release notes for v5.30.0 and earlier, along with every patch release, are on [GitHub Releases](https://github.com/prowler-cloud/prowler/releases).
+
diff --git a/docs/developer-guide/attack-paths-queries.mdx b/docs/developer-guide/attack-paths-queries.mdx
new file mode 100644
index 0000000000..2b7f69543d
--- /dev/null
+++ b/docs/developer-guide/attack-paths-queries.mdx
@@ -0,0 +1,467 @@
+---
+title: "Attack Paths Queries"
+---
+
+This guide explains how to write and maintain Prowler Attack Paths queries: the read-only openCypher queries that traverse the Cartography-ingested cloud graph to detect privilege escalation chains, network exposure, and other graph-shaped security risks.
+
+
+**New to Attack Paths?** Start with the user documentation:
+- [Attack Paths](/user-guide/tutorials/prowler-app-attack-paths) - What Attack Paths detects, how to run built-in queries, and how to explore the resulting graph.
+- [Writing Custom openCypher Queries](/user-guide/tutorials/prowler-app-attack-paths#writing-custom-opencypher-queries) - Run ad-hoc read-only queries from the Prowler App.
+
+
+## Introduction
+
+Attack Paths queries run against a property graph populated by [Cartography](https://github.com/cartography-cncf/cartography), an open-source graph ingestion framework, and enriched with Prowler findings. Every query is read-only openCypher (Version 9) so it runs on both the Neo4j and Amazon Neptune sinks.
+
+Two categories of query exist, each with a different isolation model:
+
+| | Predefined queries | Custom queries |
+| ------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------- |
+| Where they live | `api/src/backend/api/attack_paths/queries/{provider}.py` | User-supplied through the custom query API endpoint |
+| Provider isolation | `AWSAccount {id: $provider_uid}` anchor plus path connectivity | Automatic `_Provider_{uuid}` label injection by `cypher_sanitizer.py` |
+| What to write | Chain every `MATCH` from the `aws` variable | Plain Cypher, no isolation boilerplate |
+| Internal labels | Never use | Never use (system-injected) |
+
+For **predefined queries**, every node must be reachable from the `AWSAccount` root through graph traversal. That reachability is the isolation boundary.
+
+For **custom queries**, the runner injects a `_Provider_{uuid}` label into every node pattern, and a post-query filter handles edge cases, so query authors write natural Cypher without isolation boilerplate.
+
+The rest of this guide focuses on predefined queries, though the graph model, list-property handling, and compatibility rules apply to both.
+
+## The Graph Model
+
+### Cartography Schema
+
+Node labels, relationship types, and properties follow the upstream Cartography schema for each provider. Do not guess them, fetch the schema for the pinned Cartography version:
+
+```bash
+grep cartography api/pyproject.toml
+```
+
+Then read the schema for that exact tag:
+
+```text
+# Git pin (prowler-cloud/cartography@):
+https://raw.githubusercontent.com/prowler-cloud/cartography/refs/tags//docs/root/modules/{provider}/schema.md
+
+# PyPI pin (cartography==):
+https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags//docs/root/modules/{provider}/schema.md
+```
+
+The public schema reference for AWS is available at [Cartography AWS Schema](https://cartography-cncf.github.io/cartography/modules/aws/schema.html).
+
+### Prowler-Specific Additions
+
+The Prowler sync task enriches the Cartography graph with the following labels and relationships. These are not part of the upstream schema:
+
+| Label / Relationship | Description |
+| ---------------------- | ----------------------------------------------------------- |
+| `ProwlerFinding` | Finding node (`status`, `severity`, `check_id`) |
+| `Internet` | Internet sentinel node used to model public exposure |
+| `CAN_ACCESS` | `(Internet)-[:CAN_ACCESS]->(resource)` exposure edge |
+| `HAS_FINDING` | `(resource)-[:HAS_FINDING]->(:ProwlerFinding)` finding link |
+| `TRUSTS_AWS_PRINCIPAL` | Role trust relationship |
+| `STS_ASSUMEROLE_ALLOW` | Principal can assume a role |
+
+### Internal Isolation Labels
+
+The sync layer also adds internal labels used only for tenant and provider isolation: `_ProviderResource`, `_AWSResource`, `_Tenant_*`, and `_Provider_*`. These must never appear in query text, predefined or custom. The runner applies isolation automatically.
+
+## Query Structure
+
+### Provider Scoping Parameter
+
+| Parameter | Property | Used on | Purpose |
+| --------------- | -------- | ------------ | -------------------------------------- |
+| `$provider_uid` | `id` | `AWSAccount` | Scopes the query to a specific account |
+
+The runner binds `$provider_uid` automatically. Every other node is isolated by path connectivity from the `AWSAccount` anchor.
+
+### Imports
+
+```python
+from api.attack_paths.queries.types import (
+ AttackPathsQueryAttribution,
+ AttackPathsQueryDefinition,
+ AttackPathsQueryParameterDefinition,
+)
+from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL
+```
+
+Always reference `PROWLER_FINDING_LABEL` through f-string interpolation, never hardcode `"ProwlerFinding"`.
+
+### Definition Fields
+
+- **id**: kebab-case `{provider}-{category}-{description}`, e.g. `aws-ec2-privesc-passrole-iam`.
+- **name**: short, human-friendly label. Sourced queries append the reference ID: `"EC2 Instance Launch with Privileged Role (EC2-001)"`.
+- **short_description**: one sentence, no technical permissions.
+- **description**: full technical explanation, plain text.
+- **provider**: `aws`, `azure`, `gcp`, `kubernetes`, or `github`.
+- **cypher**: f-string Cypher body. Literal `{` and `}` are escaped as `{{` and `}}`.
+- **parameters**: `parameters=[]` when the query takes no input.
+- **attribution**: optional `AttackPathsQueryAttribution(text, link)` for sourced queries. The `link` uses the lowercase ID.
+
+Append the constant to the `{PROVIDER}_QUERIES` list at the bottom of the provider file.
+
+## The Predefined Query Template
+
+The canonical shape combines a principal walk, an optional target walk, deduplicated nodes, and a typed finding overlay:
+
+```python
+AWS_QUERY_NAME = AttackPathsQueryDefinition(
+ id="aws-kebab-case-name",
+ name="Label (REFERENCE_ID)",
+ short_description="One sentence.",
+ description="Full technical explanation.",
+ attribution=AttackPathsQueryAttribution(
+ text="pathfinding.cloud - REFERENCE_ID - permission",
+ link="https://pathfinding.cloud/paths/reference_id_lowercase",
+ ),
+ provider="aws",
+ cypher=f"""
+ // Find principals with the source permission
+ MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement {{effect: 'Allow'}})
+ MATCH (stmt)-[:HAS_ACTION]->(act:AWSPolicyStatementActionItem)
+ WHERE toLower(act.value) IN ['permission_lowercase', 'service:*']
+ OR act.value = '*'
+ WITH DISTINCT aws, principal, stmt, path_principal
+
+ // Pre-aggregate the statement's resource values (see "Avoiding Cartesian Products")
+ MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
+ WITH aws, path_principal, collect(DISTINCT res.value) AS res_values
+ WITH aws, path_principal, res_values, ('*' IN res_values) AS res_wildcard
+
+ // Match each target once against the in-memory resource list
+ MATCH path_target = (aws)--(target_role:AWSRole)
+ WITH path_principal, path_target, res_values, res_wildcard,
+ target_role.name AS rname, target_role.arn AS rarn
+ WHERE res_wildcard
+ OR size([rv IN res_values WHERE rv CONTAINS rname OR rarn CONTAINS rv]) > 0
+
+ WITH DISTINCT path_principal, path_target
+ WITH collect(path_principal) + collect(path_target) AS paths
+ UNWIND paths AS p
+ UNWIND nodes(p) AS n
+
+ WITH paths, collect(DISTINCT n) AS unique_nodes
+ UNWIND unique_nodes AS n
+ OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}})
+
+ RETURN paths, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr
+ """,
+ parameters=[],
+)
+```
+
+Key points:
+
+- The principal walk types the `POLICY` and `STATEMENT` hops. Both are low-fan-out (each principal has a handful of policies; each policy a handful of statements), so the typed edge lets the planner cost a cheap inline filter.
+- The `(aws)--` hub hops stay anonymous. `AWSAccount` is a high-degree node that fans out to every principal, role, policy, and resource in the account; typing those edges forces the planner to enumerate from the hub and collapses performance on multi-tenant Neptune.
+- Other relationship types appear only where the file's existing queries already use one (`TRUSTS_AWS_PRINCIPAL`, `STS_ASSUMEROLE_ALLOW`, `MEMBER_AWS_GROUP`, `HAS_EXECUTION_ROLE`).
+- The finding probe is typed `:HAS_FINDING` and left undirected. The type lets Neptune apply an inline edge filter; the missing direction matches the convention of the rest of the file.
+- Collapse duplicate rows after each permission gate with `WITH DISTINCT`, carrying only the variables needed by later clauses.
+- The `RETURN` shape `paths, dpf, dpfr` is the contract the serializer and visualizer depend on. Do not change it.
+
+## Avoiding Cartesian Products
+
+The most common performance defect in Attack Paths queries is a Cartesian product between a target set and a policy statement's resource items. When the two are written as independent `MATCH` clauses, the planner pairs every target with every resource item before any filter runs. On accounts with many IAM principals, that multiplies into hundreds of thousands of rows and the query errors or times out.
+
+### The Pattern That Causes It
+
+```cypher
+// One row per (target_role x resource_item): a Cartesian product
+MATCH path_target = (aws)--(target_role:AWSRole)
+MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
+WHERE res.value = '*'
+ OR res.value CONTAINS target_role.name
+ OR target_role.arn CONTAINS res.value
+```
+
+The two `MATCH` clauses share no relationship, so the engine enumerates all targets multiplied by all resource items, applies a non-indexable `CONTAINS` to each pair, then expands the finding overlay for every surviving row. Cost grows with `targets × resources`, and a second constrained statement (`stmt2`) multiplies it again.
+
+### The Pattern That Avoids It
+
+Collect the statement's resource values into a list once, then match each target a single time against that in-memory list:
+
+```cypher
+// Pre-aggregate the statement's resource values into a list
+MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
+WITH aws, path_principal, collect(DISTINCT res.value) AS res_values
+WITH aws, path_principal, res_values, ('*' IN res_values) AS res_wildcard
+
+// Match each target once; bind name/arn to locals so the predicate reads them once
+MATCH path_target = (aws)--(target_role:AWSRole)
+WITH path_principal, path_target, res_values, res_wildcard,
+ target_role.name AS rname, target_role.arn AS rarn
+WHERE res_wildcard
+ OR size([rv IN res_values WHERE rv CONTAINS rname OR rarn CONTAINS rv]) > 0
+```
+
+Cost now grows with `targets + resources` (linear) rather than `targets × resources`. The rewrite is a pure algebraic identity: the result set is unchanged.
+
+Guidelines:
+
+- **Aggregate resources before matching targets, not after.** `collect(DISTINCT res.value)` reduces the resource items to a single list per statement.
+- **Short-circuit the wildcard grant** with `('*' IN res_values)`. When a statement grants `*`, every target matches, so the list scan is skipped entirely.
+- **Bind `target.name` and `target.arn` to local variables** in a `WITH` before the predicate. The list comprehension then reads each once per target instead of re-reading the property store once per resource value.
+- **Use `size([... ]) > 0`, not `any(...)`.** The `any()`, `all()`, and `none()` predicate functions are not part of the openCypher specification and fail on Amazon Neptune. See [openCypher Compatibility](#opencypher-compatibility).
+- **For two-statement queries**, aggregate each statement's resources into its own list (`res_values`, `res2_values`) and combine the two `size([... ]) > 0` checks with `AND`.
+
+Every IAM privilege escalation query in `aws.py` uses this pattern. The lateral-movement variants that constrain the target with a relationship (`STS_ASSUMEROLE_ALLOW`, `TRUSTS_AWS_PRINCIPAL`) already limit the target set before the resource filter, which keeps them efficient without further aggregation.
+
+## Privilege Escalation Sub-Patterns
+
+Four `path_target` shapes cover the common escalation types. Each shares the canonical template's `path_principal`, the resource pre-aggregation, the deduplication tail, and the `RETURN`; only the `path_target` `MATCH` and its resource predicate differ.
+
+| Sub-pattern | Target | `path_target` shape | Example |
+| ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------- | ------- |
+| Self-escalation | Principal's own policies | `(aws)--(target_policy:AWSPolicy)--(principal)` | IAM-001 |
+| Lateral to user | Other IAM users | `(aws)--(target_user:AWSUser)` | IAM-002 |
+| Assume-role lateral | Assumable roles | `(aws)--(target_role:AWSRole)-[:STS_ASSUMEROLE_ALLOW]-(principal)` | IAM-014 |
+| PassRole plus service | Service-trusting roles | `(aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]-(:AWSPrincipal {arn: '{service}.amazonaws.com'})` | EC2-001 |
+
+**Multi-permission queries** (for example PassRole plus a service-create action) add permission gates before `path_target`. Reuse the per-query counter for new variables (`act2`, `policy2`, `stmt2`) and collapse rows after each gate:
+
+```cypher
+MATCH (principal)-[:POLICY]->(policy2:AWSPolicy)-[:STATEMENT]->(stmt2:AWSPolicyStatement {effect: 'Allow'})
+MATCH (stmt2)-[:HAS_ACTION]->(act2:AWSPolicyStatementActionItem)
+WHERE toLower(act2.value) IN ['service:*', 'service:createsomething']
+ OR act2.value = '*'
+WITH DISTINCT aws, principal, stmt, stmt2, path_principal
+```
+
+When a permission is an existence-only gate whose statement resource is not checked later, keep the policy and statement anonymous and carry only the variables still needed:
+
+```cypher
+MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(:AWSPolicyStatement {effect: 'Allow'})-[:HAS_ACTION]->(act3:AWSPolicyStatementActionItem)
+WHERE toLower(act3.value) IN ['service:*', 'service:othersomething']
+ OR act3.value = '*'
+WITH DISTINCT aws, principal, stmt, path_principal
+```
+
+## Network Exposure Pattern
+
+The Internet node is reached through `CAN_ACCESS` from an already-scoped resource, never as a standalone lookup:
+
+```python
+cypher=f"""
+ // Resource scoped through the account anchor
+ MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(resource:EC2Instance)
+ WHERE resource.exposed_internet = true
+
+ // Internet node reached through path connectivity from the resource
+ OPTIONAL MATCH (internet:Internet)-[can_access:CAN_ACCESS]->(resource)
+
+ WITH collect(path) AS paths, head(collect(internet)) AS internet, collect(can_access) AS can_access
+ UNWIND paths AS p
+ UNWIND nodes(p) AS n
+
+ WITH paths, internet, can_access, collect(DISTINCT n) AS unique_nodes
+ UNWIND unique_nodes AS n
+ OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}})
+
+ RETURN paths, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr,
+ internet, can_access
+"""
+```
+
+The `CAN_ACCESS` edge stays typed and directed (`-[:CAN_ACCESS]->`); that is its canonical sync-time orientation. Network-exposure queries extend the `RETURN` contract with `internet, can_access`.
+
+## Working with List-Typed Properties
+
+Some Cartography node properties carry a list of values: `AWSPolicyStatement.action`, `AWSPolicyStatement.resource`, `AWSPolicyStatement.notaction`, `AWSPolicyStatement.notresource`, `KMSKey.encryption_algorithms`, `CloudFrontDistribution.aliases`, the container-definition lists on `ECSContainerDefinition`, and many others. The graph models each such property as a set of child item nodes connected to the parent by a typed edge. Queries reach the values by traversing the edge; the parent does not carry the list as a single field.
+
+### Naming Convention
+
+For a list-typed parent property the sink stores:
+
+- **Child label**: `Item`. Example: `AWSPolicyStatement.resource` becomes `AWSPolicyStatementResourceItem`.
+- **Edge type**: `HAS_`. Example: `resource` becomes `HAS_RESOURCE`.
+- **Child property**: `value`, a single scalar string per list element. For list-of-dict properties (rare; for example `SecretsManagerSecretVersion.tags`) the child carries the original dict keys as named fields per the catalog's `field_map`.
+
+### Variable Naming for Child-Item Matches
+
+`aws.py` uses a per-query counter for each `HAS_*` traversal so chained matches stay unambiguous. The counter resets at the top of every query.
+
+| Edge | First | Second | Third |
+| ----------------- | ------ | ------- | ------- |
+| `HAS_ACTION` | `act` | `act2` | `act3` |
+| `HAS_RESOURCE` | `res` | `res2` | `res3` |
+| `HAS_NOTACTION` | `nact` | `nact2` | `nact3` |
+| `HAS_NOTRESOURCE` | `nres` | `nres2` | `nres3` |
+
+### Matching an Action
+
+To find statements that grant `iam:PassRole`, `iam:*`, or `*`, traverse the `HAS_ACTION` edge in its own `MATCH` clause and apply the predicate in the attached `WHERE`:
+
+```cypher
+MATCH (stmt:AWSPolicyStatement {effect: 'Allow'})
+MATCH (stmt)-[:HAS_ACTION]->(act:AWSPolicyStatementActionItem)
+WHERE toLower(act.value) IN ['iam:passrole', 'iam:*']
+ OR act.value = '*'
+```
+
+The literal-action list is case-folded with `toLower(act.value)` because IAM authors mix case (`iam:PassRole`, `iam:passrole`); the `*` wildcard never lower-cases.
+
+### Matching a Resource Against a Target
+
+To find statements whose resource can target a specific node, pre-aggregate the resource values and test the target against the list once (see [Avoiding Cartesian Products](#avoiding-cartesian-products)):
+
+```cypher
+MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
+WITH aws, path_principal, collect(DISTINCT res.value) AS res_values
+WITH aws, path_principal, res_values, ('*' IN res_values) AS res_wildcard
+
+MATCH path_target = (aws)--(target_role:AWSRole)
+WITH path_principal, path_target, res_values, res_wildcard,
+ target_role.name AS rname, target_role.arn AS rarn
+WHERE res_wildcard
+ OR size([rv IN res_values WHERE rv CONTAINS rname OR rarn CONTAINS rv]) > 0
+```
+
+Three predicates cover the resource cases: full wildcard (`*`), a pattern containing the target name (`arn:aws:iam::*:role/admin*`), and a pattern that is a prefix or component of the actual ARN.
+
+### Every-Item and Any-Item Predicates on a Custom Query
+
+Custom queries can express list predicates directly with pattern comprehensions. To check whether *every* item satisfies a predicate, count the counter-examples and require zero, together with a guard that ensures at least one item is attached:
+
+```cypher
+MATCH (stmt:AWSPolicyStatement)
+WHERE size([
+ (stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem)
+ WHERE NOT toLower(a.value) STARTS WITH 's3:'
+ | a
+ ]) = 0
+ AND size([(stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem) | a]) > 0
+RETURN stmt
+LIMIT 25
+```
+
+To return the list of values directly, collect them from the child items:
+
+```cypher
+MATCH (stmt:AWSPolicyStatement {effect: 'Allow'})
+OPTIONAL MATCH (stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem)
+RETURN stmt, collect(a.value) AS actions
+LIMIT 25
+```
+
+### Catalog of List Properties
+
+The provider catalog lives in `api/src/backend/tasks/jobs/attack_paths/provider_config.py` (`AWS_NORMALIZED_LISTS`). Beyond policy statements it includes KMS algorithms, ECS container-definition lists (`entry_point`, `command`, `links`, `dns_servers`, and others), CloudFront aliases, Inspector finding URL and vulnerability lists, and RDS event-subscription categories. To query a list property that is not in the catalog, add an entry there first so the sync layer materializes it. Properties absent from the catalog are serialized to a comma-delimited string and emit a one-time warning during sync.
+
+## Working with JSON-Encoded Properties
+
+Some Cartography properties represent nested objects, most notably `condition` on `AWSPolicyStatement` and `S3PolicyStatement` nodes. To keep the schema portable across graph backends, object-typed properties are stored as JSON-encoded strings:
+
+```
+'{"StringEquals":{"aws:SourceAccount":"123456789012"}}'
+```
+
+No JSON parser is available at query time, so use `CONTAINS` for substring checks against keys or known values:
+
+```cypher
+MATCH (stmt:AWSPolicyStatement)
+WHERE stmt.effect = 'Allow'
+ AND stmt.condition CONTAINS '"aws:SourceAccount"'
+RETURN stmt
+LIMIT 25
+```
+
+When a query needs to inspect the structured members of a condition (for example, to evaluate every operator and key), fetch the rows first and parse the JSON in application code. Cypher cannot navigate JSON object keys or values.
+
+## openCypher Compatibility
+
+Queries must run on both Neo4j and Amazon Neptune. Neptune implements a subset of Cypher, so several convenient constructs are unavailable. Avoid the following:
+
+| Feature | Use instead |
+| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| APOC procedures (`apoc.*`) | Real nodes and relationships in the graph |
+| Neptune extensions | Standard openCypher |
+| `any(x IN list ...)` | `size([x IN list WHERE pred]) > 0` |
+| `all(x IN list ...)` | `size([x IN list WHERE pred]) = size(list)` |
+| `none(x IN list ...)` | `size([x IN list WHERE pred]) = 0` |
+| `reduce()` | `UNWIND` plus `collect()` |
+| `FOREACH` | `WITH` plus `UNWIND` plus `SET` |
+| Regex `=~` | `toLower()` plus exact match, or `STARTS WITH` / `CONTAINS` |
+| `CALL () { UNION }` | Multi-label `OR` in `WHERE` |
+| Carried value plus aggregate expression | Project the aggregate first (`WITH principal_paths, collect(...) AS target_paths`), then combine lists in the next `WITH` |
+| `EXISTS { MATCH (pattern) WHERE pred }` | Standalone `MATCH (pattern)` plus `WHERE pred`; precede the downstream `collect(path...)` with `WITH DISTINCT ` to dedupe the joins |
+
+The carried-value-plus-aggregate rule is worth calling out because it is easy to hit. Neo4j 5.x rejects an expression that concatenates a carried list variable with an aggregate in the same projection:
+
+```cypher
+// Rejected: "Aggregation column contains implicit grouping expressions"
+WITH principal_paths + collect(DISTINCT path_target) AS paths
+```
+
+Split it into two `WITH` clauses so the aggregation resolves before the concatenation:
+
+```cypher
+WITH principal_paths, collect(DISTINCT path_target) AS target_paths
+WITH principal_paths + target_paths AS paths
+```
+
+For list-typed properties in the catalog (action, resource, and so on), traverse the `HAS_*` edges to the child item nodes rather than reading a single field; `split(...)` and comma-string predicates do not apply.
+
+## Best Practices
+
+1. **Chain every MATCH from the account anchor.** An unanchored `MATCH (role:AWSRole)` returns roles from every provider in the graph; `MATCH (aws)--(role:AWSRole)` is scoped. A second-permission `MATCH` such as `MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement)` is safe because `principal` is already bound to the account subgraph.
+2. **Pre-aggregate resource lists before matching targets** to avoid Cartesian products (see [Avoiding Cartesian Products](#avoiding-cartesian-products)).
+3. **Type the finding probe.** Always `OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}})`. The type lets Neptune apply an inline edge filter; an untyped probe scans every incident edge of high-degree nodes.
+4. **Comment each MATCH.** One inline `// ...` line per clause explaining its role.
+5. **Never use internal labels.** `_ProviderResource`, `_AWSResource`, `_Tenant_*`, and `_Provider_*` are system isolation labels and must not appear in query text.
+6. **Reach the Internet node through path connectivity** with `(internet:Internet)-[:CAN_ACCESS]->(resource)`, never as a standalone match.
+7. **Preserve the RETURN contract.** `paths, dpf, dpfr` for the standard shape; add `internet, can_access` for network-exposure queries. The serializer and visualizer depend on these names.
+
+## Naming Conventions
+
+- **ID**: kebab-case `{provider}-{category}-{description}`, e.g. `aws-ec2-privesc-passrole-iam`.
+- **Constant**: `UPPER_SNAKE_CASE` `{PROVIDER}_{CATEGORY}_{DESCRIPTION}`, e.g. `AWS_EC2_PRIVESC_PASSROLE_IAM`.
+
+## Creating a New Query
+
+New queries come from one of two input sources: a [pathfinding.cloud](https://github.com/DataDog/pathfinding.cloud) research ID (for example `ECS-001`, `GLUE-001`) or a natural-language description from the requester. The aggregated `paths.json` is too large to fetch whole; query a single path by ID:
+
+```bash
+# Fetch a single path by ID
+curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \
+ | jq '.[] | select(.id == "ecs-002")'
+
+# List all path IDs and names
+curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \
+ | jq -r '.[] | "\(.id): \(.name)"'
+```
+
+Then follow these steps:
+
+1. **Read the queries module first** to match the existing style:
+
+ ```text
+ api/src/backend/api/attack_paths/queries/
+ ├── __init__.py
+ ├── types.py # dataclass definitions
+ ├── registry.py
+ └── {provider}.py
+ ```
+
+2. **Fetch the Cartography schema for the pinned version.** Do not guess labels, properties, or relationships. See [The Graph Model](#the-graph-model).
+
+3. **Build the query** from the canonical template plus the appropriate sub-pattern (privilege escalation or network exposure). Pre-aggregate resource lists, traverse `HAS_*` edges for list-typed properties, and keep the `RETURN` contract.
+
+4. **Register** the constant in the `{PROVIDER}_QUERIES` list at the bottom of the provider file.
+
+5. **Verify compatibility** against the [openCypher Compatibility](#opencypher-compatibility) rules, and confirm the query parses and runs on Neo4j before it reaches Neptune.
+
+
+AI assistants connected through Prowler MCP Server can fetch the exact Cartography schema for the active scan with the `prowler_get_attack_paths_cartography_schema` tool, which guarantees that generated queries match the schema version pinned by the running Prowler release.
+
+
+## Reference
+
+- **pathfinding.cloud**: [github.com/DataDog/pathfinding.cloud](https://github.com/DataDog/pathfinding.cloud) (use `curl | jq`; the aggregated `paths.json` is too large for a single fetch).
+- **Cartography AWS schema**: [cartography-cncf.github.io/cartography/modules/aws/schema.html](https://cartography-cncf.github.io/cartography/modules/aws/schema.html).
+- **Neptune openCypher compliance**: [docs.aws.amazon.com/neptune/latest/userguide/feature-opencypher-compliance.html](https://docs.aws.amazon.com/neptune/latest/userguide/feature-opencypher-compliance.html).
+- **Neptune openCypher rewrites**: [docs.aws.amazon.com/neptune/latest/userguide/migration-opencypher-rewrites.html](https://docs.aws.amazon.com/neptune/latest/userguide/migration-opencypher-rewrites.html).
+- **openCypher specification**: [github.com/opencypher/openCypher](https://github.com/opencypher/openCypher).
diff --git a/docs/developer-guide/aws-details.mdx b/docs/developer-guide/aws-details.mdx
index 67a9f15256..cc8225c45e 100644
--- a/docs/developer-guide/aws-details.mdx
+++ b/docs/developer-guide/aws-details.mdx
@@ -40,7 +40,7 @@ The AWS provider implementation follows the general [Provider structure](/develo
- **Key AWS Responsibilities:**
- Receives an `AwsProvider` instance to access session, identity, and configuration.
- Manages clients for all services by regions.
- - Provides `__threading_call__` method to make boto3 calls in parallel. By default, this calls are made by region, but it can be overridden with the first parameter of the method and use by resource.
+ - Provides `__threading_call__` method to make boto3 calls in parallel. By default, these calls are made by region, but it can be overridden with the first parameter of the method and use by resource.
- Exposes common audit context (`audited_account`, `audited_account_arn`, `audited_partition`, `audited_resources`) to subclasses.
### Exception Handling
@@ -60,7 +60,7 @@ The generic service pattern is described in [service page](/developer-guide/serv
- Directly in the code, in location [`prowler/providers/aws/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/aws/services)
- In the [Prowler Hub](https://hub.prowler.com/). For a more human-readable view.
-The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used accross all AWS services.
+The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used across all AWS services.
### AWS Service Common Patterns
@@ -69,7 +69,7 @@ The best reference to understand how to implement a new service is following the
- The constructor (`__init__`) always calls `super().__init__` with the service name and provider (e.g. `super().__init__(__class__.__name__, provider))`). Ensure that the service name in boto3 is the same that you use in the constructor. Usually is used the `__class__.__name__` to get the service name because it is the same as the class name.
- Resource containers **must** be initialized in the constructor. They should be dictionaries, with the key being the resource ARN or equivalent unique identifier and the value being the resource object.
- Resource discovery and attribute collection are parallelized using `self.__threading_call__`, typically by region or resource, for performance. The first parameter of the method is the iterator, if not provided, it will be the region; but if present indicate an array of the resources to be processed.
-- Resource filtering is consistently enforced using `self.audit_resources` attribute and `is_resource_filtered` function, it is used to see if user has provided some resource that is not in the audit scope, so we can skip it in the service logic. Normally it is used befor storing the resource in the service container as follows: `if not self.audit_resources or (is_resource_filtered(resource["arn"], self.audit_resources)):`.
+- Resource filtering is consistently enforced using `self.audit_resources` attribute and `is_resource_filtered` function, it is used to see if user has provided some resource that is not in the audit scope, so we can skip it in the service logic. Normally it is used before storing the resource in the service container as follows: `if not self.audit_resources or (is_resource_filtered(resource["arn"], self.audit_resources)):`.
- All AWS resources are represented as Pydantic `BaseModel` classes, providing type safety and structured access to resource attributes.
- AWS API calls are wrapped in try/except blocks, with specific handling for `ClientError` and generic exceptions, always logging errors.
- If ARN is not present for some resource, it can be constructed using string interpolation, always including partition, service, region, account, and resource ID.
@@ -163,7 +163,7 @@ When you instantiate `Check_Report_AWS`, you must provide the check metadata and
If the resource object does not contain the required attributes, you must set them manually in the check logic.
-Other attributes are inherited from the `Check_Report` class, from that ones you **always** have to set the `status` and `status_extended` attributes in the check logic.
+Other attributes are inherited from the `Check_Report` class, from those you **always** have to set the `status` and `status_extended` attributes in the check logic.
#### Example Usage
diff --git a/docs/developer-guide/azure-details.mdx b/docs/developer-guide/azure-details.mdx
index 790c430b4c..aa576eae68 100644
--- a/docs/developer-guide/azure-details.mdx
+++ b/docs/developer-guide/azure-details.mdx
@@ -58,7 +58,7 @@ The generic service pattern is described in [service page](/developer-guide/serv
- Directly in the code, in location [`prowler/providers/azure/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/azure/services)
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
-The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used accross all Azure services.
+The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used across all Azure services.
### Azure Service Common Patterns
diff --git a/docs/developer-guide/checks.mdx b/docs/developer-guide/checks.mdx
index e4596d883e..aceba7fd93 100644
--- a/docs/developer-guide/checks.mdx
+++ b/docs/developer-guide/checks.mdx
@@ -173,7 +173,7 @@ else:
### Resource Identification in Prowler
-Each check **must** populate the report with an unique identifier for the audited resource. This identifier or identifiers are going to depend on the provider and the resource that is being audited. Here are the criteria for each provider:
+Each check **must** populate the report with a unique identifier for the audited resource. This identifier or identifiers are going to depend on the provider and the resource that is being audited. Here are the criteria for each provider:
- AWS
- Amazon Resource ID — `report.resource_id`.
diff --git a/docs/developer-guide/configurable-checks.mdx b/docs/developer-guide/configurable-checks.mdx
index d509be568e..f0cc268886 100644
--- a/docs/developer-guide/configurable-checks.mdx
+++ b/docs/developer-guide/configurable-checks.mdx
@@ -134,6 +134,7 @@ Only fields with a numeric range, a fixed value set, or a length cap are listed.
| `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 |
+| `max_ec2_instance_stopped_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 | |
diff --git a/docs/developer-guide/documentation.mdx b/docs/developer-guide/documentation.mdx
index fa1f648a23..58be8d3f32 100644
--- a/docs/developer-guide/documentation.mdx
+++ b/docs/developer-guide/documentation.mdx
@@ -9,9 +9,9 @@ Prowler documentation is built using [Mintlify](https://www.mintlify.com/docs),
The Prowler documentation is organized into several sections. The main ones are:
-- **Getting Started**: Provides an overview of the Prowler platform and its different solutions, including Prowler Cloud/App, Prowler CLI, Prowler MCP Server, Prowler Hub, and Prowler Lighthouse AI. This section helps new users understand which Prowler solution best fits their needs and includes product comparisons.
+- **Getting Started**: Provides an overview of the Prowler platform and its two product families, Prowler Products (Prowler Cloud, Prowler Hub, Prowler MCP, Prowler Lighthouse AI) and Open Source (Prowler CLI, Prowler Local Server). This section helps new users understand which Prowler solution best fits their needs and includes product comparisons.
-- **Guides**: Contains practical tutorials and how-to guides organized by product (Prowler Cloud/App, CLI) and provider (AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub, etc.). This section covers authentication, integrations, compliance, and advanced usage scenarios.
+- **Guides**: Contains practical tutorials and how-to guides organized by product (Prowler Cloud, CLI) and provider (AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub, etc.). This section covers authentication, integrations, compliance, and advanced usage scenarios.
- **Developer Guide**: Documentation for contributors looking to extend Prowler functionality. This includes guides on creating providers, services, checks, output formats, integrations, and compliance frameworks. Provider-specific implementation details and testing strategies are also covered here.
diff --git a/docs/developer-guide/end2end-testing.mdx b/docs/developer-guide/end2end-testing.mdx
index fbba9b2b52..9b7402b5f3 100644
--- a/docs/developer-guide/end2end-testing.mdx
+++ b/docs/developer-guide/end2end-testing.mdx
@@ -3,11 +3,11 @@ title: 'End-to-End Tests for Prowler App'
description: 'Write Playwright end-to-end tests for Prowler App covering user journeys, Page Object Models, storage state reuse, and organizing spec files by feature area.'
---
-End-to-end (E2E) tests validate complete user flows in Prowler App (UI + API). These tests are implemented with [Playwright](https://playwright.dev/) under the `ui/tests` folder and are designed to run against a Prowler App environment.
+End-to-end (E2E) tests validate complete user flows in Prowler Local Server (UI + API). These tests are implemented with [Playwright](https://playwright.dev/) under the `ui/tests` folder and are designed to run against a Prowler Local Server environment.
## General Recommendations
-When adding or maintaining E2E tests for Prowler App, follow these guidelines:
+When adding or maintaining E2E tests for Prowler Local Server, follow these guidelines:
1. **Test real user journeys**
Focus on full workflows (for example, sign-up → login → add provider → launch scan) instead of low-level UI details already covered by unit or integration tests.
@@ -20,8 +20,8 @@ When adding or maintaining E2E tests for Prowler App, follow these guidelines:
3. **Use a Page Model (Page Object Model)**
- Encapsulate selectors and common actions in page classes instead of repeating them in each test.
- Leverage and extend the existing Playwright page models in `ui/tests`—such as `ProvidersPage`, `ScansPage`, and others—which are all based on the shared `BasePage`.
- - Page models for Prowler App pages should be placed in their respective entity folders (for example, `ui/tests/providers/providers-page.ts`).
- - Page models for external pages (not part of Prowler App) should be grouped in the `external` folder (for example, `ui/tests/external/github-page.ts`).
+ - Page models for Prowler Local Server pages should be placed in their respective entity folders (for example, `ui/tests/providers/providers-page.ts`).
+ - Page models for external pages (not part of Prowler Local Server) should be grouped in the `external` folder (for example, `ui/tests/external/github-page.ts`).
- This approach improves readability, reduces duplication, and makes refactors safer.
4. **Reuse authentication states (StorageState)**
@@ -194,7 +194,7 @@ When adding or maintaining E2E tests for Prowler App, follow these guidelines:
## Running Prowler Tests
-E2E tests for Prowler App run from the `ui` project using Playwright. The Playwright configuration lives in `ui/playwright.config.ts` and defines:
+E2E tests for Prowler Local Server run from the `ui` project using Playwright. The Playwright configuration lives in `ui/playwright.config.ts` and defines:
- `testDir: "./tests"` – location of E2E test files (relative to the `ui` project root, so `ui/tests`).
- `webServer` – how to start the Next.js development server and connect to Prowler API.
@@ -226,7 +226,7 @@ Before running E2E tests:
- Start Prowler API so it is reachable on that URL (for example, via `docker-compose-dev.yml` or the development orchestration used locally).
- If a different API URL is required, set `UI_API_BASE_URL` accordingly before running the tests.
-- **Ensure Prowler App UI is available**
+- **Ensure Prowler Local Server UI is available**
- Playwright automatically starts the Next.js server through the `webServer` block in `playwright.config.ts` (`pnpm run dev` by default).
- If the UI is already running on `http://localhost:3000`, Playwright will reuse the existing server when `reuseExistingServer` is `true`.
@@ -247,7 +247,7 @@ Before running E2E tests:
### Executing Tests
-To execute E2E tests for Prowler App:
+To execute E2E tests for Prowler Local Server:
1. **Run the full E2E suite (headless)**
diff --git a/docs/developer-guide/environment-variables.mdx b/docs/developer-guide/environment-variables.mdx
index c8532a2a66..e5b581ac77 100644
--- a/docs/developer-guide/environment-variables.mdx
+++ b/docs/developer-guide/environment-variables.mdx
@@ -3,7 +3,7 @@ title: 'Environment Variable Naming Conventions'
description: 'Namespace Prowler App, API, SDK, and MCP Server environment variables with component prefixes like UI_ and API_ to avoid conflicts in a shared .env file.'
---
-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.
+Prowler is a monorepo composed of several runtime components — Prowler Local Server (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
@@ -11,7 +11,7 @@ Each component owns a dedicated prefix for the environment variables it reads:
| Component | Prefix | Status |
|-----------|--------|--------|
-| Prowler App (web UI) | `UI_` | Adopted |
+| Prowler Local Server (web UI) | `UI_` | Adopted |
| Prowler API (backend) | `API_` | Planned |
| Prowler SDK | `SDK_` | Planned |
| Prowler MCP Server | `MCP_` | Planned |
@@ -22,11 +22,11 @@ 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.
+- **Reduced accidental exposure:** For Prowler Local Server, scoping browser-facing configuration under one intentional prefix prevents server-only values from leaking into the client bundle.
-## Prowler App
+## Prowler Local Server
-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).
+Prowler Local Server 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:
@@ -37,25 +37,28 @@ The former build-time variables map to the new runtime variables as follows:
| `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` |
+| `NEXT_PUBLIC_IS_CLOUD_ENV` | `UI_CLOUD_ENABLED` |
-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.
+`UI_CLOUD_ENABLED` is a plain runtime boolean flag that enables Prowler Cloud behavior when set to the exact string `"true"` and defaults to off; unlike the other renamed variables it has no legacy fallback, so `NEXT_PUBLIC_IS_CLOUD_ENV` is no longer read.
+
+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 Prowler Local Server's runtime configuration.
## Enabling Third-Party Integrations
-Prowler App gates each optional third-party integration behind an explicit enable flag. When an integration is configured through its new `UI_*` variables, it loads only when its flag is set to the exact string `"true"`; any other value, including unset, leaves it off. This default-off behavior keeps a deployment free of third-party egress unless it opts in. Deployments still using the deprecated legacy variable names keep loading without the flag, for backward compatibility (see [Deprecated Names](#deprecated-names)).
+Prowler Local Server gates each optional third-party integration behind an explicit enable flag. When an integration is configured through its new `UI_*` variables, it loads only when its flag is set to the exact string `"true"`; any other value, including unset, leaves it off. This default-off behavior keeps a deployment free of third-party egress unless it opts in. Deployments still using the deprecated legacy variable names keep loading without the flag, for backward compatibility (see [Deprecated Names](#deprecated-names)).
| Integration | Enable flag | Required configuration when enabled |
|-------------|-------------|-------------------------------------|
-| Sentry (error monitoring) | `UI_SENTRY_ENABLE` | `UI_SENTRY_DSN` |
-| Google Tag Manager | `UI_GOOGLE_TAG_MANAGER_ENABLE` | `UI_GOOGLE_TAG_MANAGER_ID` |
-| PostHog (product analytics) | `UI_POSTHOG_ENABLE` | `UI_POSTHOG_KEY` and `UI_POSTHOG_HOST` |
+| Sentry (error monitoring) | `UI_SENTRY_ENABLED` | `UI_SENTRY_DSN` |
+| Google Tag Manager | `UI_GOOGLE_TAG_MANAGER_ENABLED` | `UI_GOOGLE_TAG_MANAGER_ID` |
+| PostHog (product analytics) | `UI_POSTHOG_ENABLED` | `UI_POSTHOG_KEY` and `UI_POSTHOG_HOST` |
-When an integration is enabled but its required configuration is missing, Prowler App fails fast at server startup with a clear error, so a misconfigured container never starts silently. A new `UI_*` value set while its enable flag is not `"true"` is ignored, and the server logs a one-time startup warning noting that the integration will not load. Legacy names follow the backward-compatible rule described in [Deprecated Names](#deprecated-names).
+When an integration is enabled but its required configuration is missing, Prowler Local Server fails fast at server startup with a clear error, so a misconfigured container never starts silently. A new `UI_*` value set while its enable flag is not `"true"` is ignored, and the server logs a one-time startup warning noting that the integration will not load. Legacy names follow the backward-compatible rule described in [Deprecated Names](#deprecated-names).
-PostHog support is currently limited to configuration validation: Prowler App reads and validates the PostHog variables but does not yet load a PostHog client.
+PostHog support is currently limited to configuration validation: Prowler Local Server reads and validates the PostHog variables but does not yet load a PostHog client.
-Configuring an integration through the new `UI_*` variables now requires its enable flag. A deployment that adopted `UI_SENTRY_DSN` or `UI_GOOGLE_TAG_MANAGER_ID` must also set `UI_SENTRY_ENABLE=true` or `UI_GOOGLE_TAG_MANAGER_ENABLE=true` to keep the integration active. Deployments still using the legacy names (`NEXT_PUBLIC_*`, or `POSTHOG_KEY` and `POSTHOG_HOST`) keep working without the flag.
+Configuring an integration through the new `UI_*` variables now requires its enable flag. A deployment that adopted `UI_SENTRY_DSN` or `UI_GOOGLE_TAG_MANAGER_ID` must also set `UI_SENTRY_ENABLED=true` or `UI_GOOGLE_TAG_MANAGER_ENABLED=true` to keep the integration active. Deployments still using the legacy names (`NEXT_PUBLIC_*`, or `POSTHOG_KEY` and `POSTHOG_HOST`) keep working without the flag.
## Upcoming Breaking Change
@@ -68,5 +71,5 @@ Prowler API, Prowler SDK, and Prowler MCP Server have not yet adopted the conven
## 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_*` names — and, for PostHog, the unprefixed `POSTHOG_KEY` and `POSTHOG_HOST` — are deprecated but stay backward compatible: they are read at runtime regardless of the enable flag, so an existing deployment keeps its integration active without opting in. The new `UI_*` names, by contrast, load only when the matching enable flag is set to `"true"`. These legacy names will be removed in a future release, so migrate to the `UI_*` runtime variables — and set the enable flag — on the running container.
+- **Prowler Local Server:** 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_*` names — and, for PostHog, the unprefixed `POSTHOG_KEY` and `POSTHOG_HOST` — are deprecated but stay backward compatible: they are read at runtime regardless of the enable flag, so an existing deployment keeps its integration active without opting in. The new `UI_*` names, by contrast, load only when the matching enable flag is set to `"true"`. These legacy names will be removed in a future release, so migrate to the `UI_*` runtime variables — and set the enable flag — 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).
diff --git a/docs/developer-guide/gcp-details.mdx b/docs/developer-guide/gcp-details.mdx
index 108a66af8e..4c635b5cad 100644
--- a/docs/developer-guide/gcp-details.mdx
+++ b/docs/developer-guide/gcp-details.mdx
@@ -103,7 +103,7 @@ The generic service pattern is described in [service page](/developer-guide/serv
- Directly in the code, in location [`prowler/providers/gcp/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/gcp/services)
- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
-The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used accross all GCP services.
+The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used across all GCP services.
### GCP Service Common Patterns
@@ -114,7 +114,7 @@ The best reference to understand how to implement a new service is following the
- Only projects with the API enabled are included in the audit scope.
- Resource discovery and attribute collection can be parallelized using `self.__threading_call__`, typically by region/zone or resource.
- All GCP resources are represented as Pydantic `BaseModel` classes, providing type safety and structured access to resource attributes.
-- Each GCP API calls are wrapped in try/except blocks, always logging errors.
+- Each GCP API call is wrapped in try/except blocks, always logging errors.
- **Retry Configuration**: All `request.execute()` calls must include `num_retries=DEFAULT_RETRY_ATTEMPTS` for automatic retry on rate limiting errors (HTTP 429).
- Tags and additional attributes that cannot be retrieved from the default call should be collected and stored for each resource using dedicated methods and threading.
@@ -162,7 +162,7 @@ When you instantiate `Check_Report_GCP`, you must provide the check metadata and
- Defaults to "global" if none are available.
All these attributes can be overridden by passing the corresponding argument to the constructor. If the resource object does not contain the required attributes, you must set them manually.
-Others attributes are inherited from the `Check_Report` class, from that ones you **always** have to set the `status` and `status_extended` attributes in the check logic.
+Other attributes are inherited from the `Check_Report` class, from those you **always** have to set the `status` and `status_extended` attributes in the check logic.
#### Example Usage
diff --git a/docs/developer-guide/introduction.mdx b/docs/developer-guide/introduction.mdx
index 3182f00be5..f9d18db69f 100644
--- a/docs/developer-guide/introduction.mdx
+++ b/docs/developer-guide/introduction.mdx
@@ -46,6 +46,9 @@ Prowler is constantly evolving. Contributions to checks, services, or integratio
Prowler can work with other tools and platforms through integrations.
+
+ Want to detect new privilege escalation or exposure patterns? Contribute read-only openCypher queries that traverse the cloud graph.
+
Propose brand-new features or enhancements to existing ones, or help implement community-requested improvements.
@@ -73,7 +76,7 @@ Remember, our community is here to help! If you need guidance, do not hesitate t
-## Setting up your development environment
+## Setting Up Your Development Environment
### Prerequisites
@@ -241,7 +244,7 @@ prowler/
├── README.md # Project overview and getting started
├── Makefile # Common development commands
├── Dockerfile # SDK Docker container
-├── docker-compose.yml # Prowler App Docker compose
+├── docker-compose.yml # Prowler Local Server Docker compose
└── ... # Other supporting files
```
diff --git a/docs/developer-guide/lighthouse-architecture.mdx b/docs/developer-guide/lighthouse-architecture.mdx
index 533f11b4c2..5772b07566 100644
--- a/docs/developer-guide/lighthouse-architecture.mdx
+++ b/docs/developer-guide/lighthouse-architecture.mdx
@@ -133,7 +133,7 @@ The MCP client manages connections to the Prowler MCP Server using a singleton p
- **Connection Management**: Retry logic with configurable attempts and delays
- **Tool Discovery**: Fetches available tools from MCP server on initialization
-- **Authentication Injection**: Automatically adds JWT tokens to `prowler_app_*` tool calls
+- **Authentication Injection**: Automatically adds JWT tokens to `prowler_*` tool calls
- **Reconnection**: Supports forced reconnection after server restarts
Key constants:
@@ -142,10 +142,14 @@ Key constants:
- `RECONNECT_INTERVAL_MS`: 5 minutes before retry after failure
```typescript
-// Authentication injection for Prowler App tools
+// Authentication injection for core prowler_ tools (Hub/Docs excluded)
private handleBeforeToolCall = ({ name, args }) => {
- // Only inject auth for prowler_app_* tools (user-specific data)
- if (!name.startsWith("prowler_app_")) {
+ // Only inject auth for prowler_* tools (user-specific data).
+ // The legacy prowler_app_ prefix is also accepted for a resilient rollout.
+ if (
+ !name.startsWith("prowler_") &&
+ !name.startsWith("prowler_app_")
+ ) {
return { args };
}
@@ -308,15 +312,15 @@ MCP tools are organized into three namespaces based on authentication requiremen
| Namespace | Auth Required | Description |
|-----------|---------------|-------------|
-| `prowler_app_*` | Yes (JWT) | Prowler Cloud/App tools for findings, providers, scans, resources |
+| `prowler_*` | Yes (JWT) | Prowler Cloud, Prowler Private Cloud, and Prowler Local Server tools for findings, providers, scans, resources |
| `prowler_hub_*` | No | Security checks catalog, compliance frameworks |
| `prowler_docs_*` | No | Documentation search and retrieval |
### Authentication Flow
-1. User authenticates with Prowler App, receiving a JWT token
+1. User authenticates with Prowler Local Server, receiving a JWT token
2. Token is stored in session and propagated via `authContextStorage`
-3. MCP client injects `Authorization: Bearer ` header for `prowler_app_*` calls
+3. MCP client injects `Authorization: Bearer ` header for `prowler_*` calls
4. MCP Server validates token and applies RLS filtering
### Tool Execution Pattern
@@ -324,7 +328,7 @@ MCP tools are organized into three namespaces based on authentication requiremen
The agent uses meta-tools rather than direct tool registration:
```
-Agent needs data → describe_tool("prowler_app_search_findings")
+Agent needs data → describe_tool("prowler_search_findings")
→ Returns parameter schema → execute_tool with parameters
→ MCP client adds auth header → MCP Server executes
→ Results returned to agent → Agent continues reasoning
diff --git a/docs/developer-guide/llm-details.mdx b/docs/developer-guide/llm-details.mdx
index 93c117f90b..b76e5058d5 100644
--- a/docs/developer-guide/llm-details.mdx
+++ b/docs/developer-guide/llm-details.mdx
@@ -102,4 +102,4 @@ The LLM provider seamlessly integrates with Prowler's existing infrastructure:
- **Output Formats**: Supports all Prowler output formats (JSON, CSV, HTML, etc.)
- **Compliance Frameworks**: Integrates with Prowler's compliance reporting
- **Fixer Integration**: Supports automated remediation recommendations
-- **Dashboard Integration**: Compatible with Prowler App for centralized management
+- **Dashboard Integration**: Compatible with Prowler Cloud and Prowler Local Server for centralized management
diff --git a/docs/developer-guide/mcp-server.mdx b/docs/developer-guide/mcp-server.mdx
index 02f8d01679..de33258e21 100644
--- a/docs/developer-guide/mcp-server.mdx
+++ b/docs/developer-guide/mcp-server.mdx
@@ -19,11 +19,15 @@ The Prowler MCP Server brings the entire Prowler ecosystem to AI assistants thro
The server follows a modular architecture with three independent sub-servers:
-| Sub-Server | Auth Required | Description |
-|------------|---------------|-------------|
-| Prowler App | Yes | Full access to Prowler Cloud and Self-Managed features |
-| Prowler Hub | No | Security checks catalog with **over 1000 checks**, fixers, and **70+ compliance frameworks** |
-| Prowler Documentation | No | Full-text search and retrieval of official documentation |
+| Sub-Server | Tool Prefix | Auth Required | Description |
+|------------|-------------|---------------|-------------|
+| Prowler | `prowler_` | Yes | Full access to Prowler Cloud, Prowler Private Cloud, and Prowler Local Server features |
+| Prowler Hub | `prowler_hub_` | No | Security checks catalog with **over 2,000 checks**, fixers, and **70+ compliance frameworks** |
+| Prowler Documentation | `prowler_docs_` | No | Full-text search and retrieval of official documentation |
+
+
+The core Prowler sub-server is served under the `prowler_` tool prefix, while its source lives in the `prowler_app/` module for historical reasons. Tool names use the prefix; import paths use the module.
+
For a complete list of tools and their descriptions, see the [Tools Reference](/getting-started/basic-usage/prowler-mcp-tools).
@@ -54,9 +58,9 @@ mcp_server/prowler_mcp_server/
The MCP Server uses two patterns for tool registration:
1. **Direct Decorators** (Prowler Hub/Docs): Tools are registered using `@mcp.tool()` decorators
-2. **Auto-Discovery** (Prowler App): All public methods of `BaseTool` subclasses are auto-registered
+2. **Auto-Discovery** (`prowler_app`): All public methods of `BaseTool` subclasses are auto-registered
-## Adding Tools to Prowler App
+## Adding Tools to the `prowler_app` Sub-Server
### Step 1: Create the Tool Class
@@ -349,7 +353,7 @@ result = await self.api_client.poll_task_until_complete(
### Tool Docstrings
-Tool docstrings become description that is going to be read by the LLM. Provide clear usage instructions and common workflows:
+Tool docstrings become the description that is going to be read by the LLM. Provide clear usage instructions and common workflows:
```python
async def search_items(self, status: str = Field(...)) -> dict:
@@ -414,7 +418,7 @@ uv run prowler-mcp
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8000
# Run with environment variables
-PROWLER_APP_API_KEY="pk_xxx" uv run prowler-mcp
+PROWLER_API_KEY="pk_xxx" uv run prowler-mcp
```
For complete installation and deployment options, see:
@@ -423,6 +427,150 @@ For complete installation and deployment options, see:
For development I recommend to use the [Model Context Protocol Inspector](https://github.com/modelcontextprotocol/inspector) as MCP client to test and debug your tools.
+## Testing
+
+Tests live in `mcp_server/tests/`, mirroring the source tree, and use the `test_*.py`
+prefix (the same convention as the API, not the SDK's `*_test.py` suffix).
+
+From `mcp_server/`:
+
+```bash
+cd mcp_server
+
+uv run pytest # Whole suite
+uv run pytest tests/prowler_app/models # One area
+uv run pytest --cov=./prowler_mcp_server # With coverage
+```
+
+From the repository root:
+
+```bash
+make test-mcp # Runs the MCP suite exactly as CI does
+```
+
+Async tests need no marker — `asyncio_mode` is set to `auto`.
+
+### Reading the Coverage Numbers
+
+
+Coverage here has a high floor that means nothing. `coverage.py` measures
+*statements*, and in a Pydantic model module nearly every statement is a class-body
+field declaration that runs at **import** time. `prowler_app/server.py` imports
+every tool module — and therefore every model module — when it is first imported,
+so all of those declarations execute and count as covered before a single test runs.
+
+Importing the package and executing no tests at all already reports **36% overall**,
+with individual model modules between 54% and 84%. A model module sitting at ~68%
+with no tests written for it has **none** of its behaviour covered: the covered lines
+are its imports, `class` statements and `Field(...)` declarations, and the missing
+ranges are its `from_api_response()` bodies.
+
+Judge a module against that import-only floor, not against zero, and do not set a
+Codecov target from the raw total.
+
+
+### Shared Fixtures
+
+All fixtures live in `mcp_server/tests/conftest.py`. Three are autouse and apply to
+every test: the environment is pinned to deterministic values, real socket
+connections are blocked, and the API client singleton registry is snapshotted and
+restored.
+
+| Fixture | What it gives you |
+|---------|-------------------|
+| `mock_api_client` | The API client singleton with its transport mocked. The workhorse. |
+| `mock_router` | Route registry and request recorder |
+| `mcp_root_server` | The mounted root server, for in-memory client tests |
+| `health_client` | Starlette `TestClient` for the `/health` route |
+| `http_request_headers` | Injects request headers for HTTP-transport auth tests |
+| `hub_router` / `docs_router` | Mock the Hub and Docs sub-servers' sync HTTP clients |
+| `api_client` / `isolated_api_client` | The live singleton / a freshly-constructed one |
+
+Helpers live in `mcp_server/tests/helpers/`: JSON:API document builders
+(`jsonapi.py`), the `MockRouter` (`http.py`), tool-contract assertions
+(`assertions.py`) and fake credentials (`tokens.py`).
+
+### Writing a Tool Test
+
+Drive tools through an in-memory MCP client, and open the client inside the test —
+FastMCP warns that holding a client in a fixture causes event-loop problems.
+
+```python
+from fastmcp import Client
+
+from tests.helpers.jsonapi import jsonapi_collection, jsonapi_resource
+
+FINDING_ATTRIBUTES = {
+ "uid": "prowler-aws-s3_bucket_public_access-123456789012-us-east-1-my-bucket",
+ "status": "FAIL",
+ "severity": "high",
+ "status_extended": "S3 bucket my-bucket is publicly accessible.",
+ "delta": "new",
+ "muted": False,
+ "muted_reason": None,
+ "check_metadata": {"checkid": "s3_bucket_public_access"},
+}
+
+
+async def test_search_without_dates_queries_the_latest_scan_endpoint(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """With no date range the tool targets the cheaper `/findings/latest`."""
+ mock_router.add(
+ "GET",
+ "/api/v1/findings/latest",
+ json=jsonapi_collection(
+ [jsonapi_resource("findings", "f1", FINDING_ATTRIBUTES)]
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool("prowler_search_security_findings", {})
+
+ assert result.data["findings"][0]["check_id"] == "s3_bucket_public_access"
+ assert mock_router.paths() == ["GET /api/v1/findings/latest"]
+```
+
+The exemplar suite covers `findings` end to end — `tests/prowler_app/models/test_findings.py`
+and `tests/prowler_app/tools/test_findings.py`. It is deliberately one feature
+across both layers rather than a scattering of unrelated samples, and `findings`
+is the feature that exercises the whole foundation: two-tier models, nested
+sub-models, both relationship shapes, endpoint switching on a date range,
+list-to-CSV filter encoding, and a tool that returns prose instead of a model.
+
+Note the two files share a name. That is why `__init__.py` is required in every
+`tests/` subdirectory here — without it they would collide on import.
+
+
+Tool parameters are declared with pydantic `Field(default=...)`, and only FastMCP's
+tool wrapper resolves those defaults. Calling a tool method directly with an
+argument omitted leaves it as a raw `FieldInfo` object, which is truthy — so a
+filter such as `if email:` silently builds a query out of the `FieldInfo` repr.
+Call tools through the client, or pass every argument explicitly.
+
+
+### Why the API Key Is Pinned, Not Stripped
+
+`prowler_app/server.py` builds every tool at import time. Constructing a tool
+reaches `ProwlerAppAuth`, which raises when `PROWLER_API_KEY` is missing, and
+`load_all_tools` swallows that error per tool class. The result is that the whole
+`prowler_*` namespace registers **zero** tools while the server still logs
+"Successfully mounted Prowler tools server".
+
+The suite therefore pins a fake key in `[tool.pytest_env]`, which is applied before
+any test module is imported, and `tests/test_server.py` asserts each namespace is
+non-empty so this failure can never return silently.
+
+
+`ProwlerAppAuth` resolves `PROWLER_MCP_TRANSPORT_MODE` and `API_BASE_URL` in its
+default arguments, which Python evaluates once at module import. `monkeypatch.setenv`
+cannot change them — pass `mode=` and `base_url=` explicitly in auth tests.
+
+
+For the full set of rules and templates, see the
+[`prowler-test-mcp` skill](https://github.com/prowler-cloud/prowler/blob/master/skills/prowler-test-mcp/SKILL.md)
+and the [official FastMCP testing guide](https://gofastmcp.com/development/tests).
+
## Related Documentation
diff --git a/docs/developer-guide/provider.mdx b/docs/developer-guide/provider.mdx
index 7dbdeac01d..7ff5cfb892 100644
--- a/docs/developer-guide/provider.mdx
+++ b/docs/developer-guide/provider.mdx
@@ -32,7 +32,7 @@ Before implementing a new provider, you need to determine which type it belongs
#### Decision Criteria
-Once you have decided the provider you want or need to add to Prowler, the next step is to study how to retrieve data from it. Based on that, the provider will fall into one of the following types: SDK, API or Tool/Wrapper (maybe in the future there will be new types but for now this are the only ones).
+Once you have decided the provider you want or need to add to Prowler, the next step is to study how to retrieve data from it. Based on that, the provider will fall into one of the following types: SDK, API or Tool/Wrapper (maybe in the future there will be new types but for now these are the only ones).
**Choose SDK Provider if:**
diff --git a/docs/developer-guide/prowler-studio.mdx b/docs/developer-guide/prowler-studio.mdx
index 76b9f57b2f..89b0392b72 100644
--- a/docs/developer-guide/prowler-studio.mdx
+++ b/docs/developer-guide/prowler-studio.mdx
@@ -6,7 +6,7 @@ description: 'Prowler Studio is a Claude Code workflow that generates consistent
**Prowler Studio is an AI workflow that ensures Claude Code follows Prowler's skills, guardrails, and best practices when creating new security checks.** What lands in the resulting pull request is consistent, tested, and ready for human review — not half-correct boilerplate that needs to be rewritten.
-**Contributor Tool**: Prowler Studio is a workflow for advanced contributors adding new Prowler security checks. It is not part of Prowler Cloud, Prowler App, or Prowler CLI.
+**Contributor Tool**: Prowler Studio is a workflow for advanced contributors adding new Prowler security checks. It is not part of Prowler Cloud, Prowler Local Server, or Prowler CLI.
diff --git a/docs/developer-guide/security-compliance-framework.mdx b/docs/developer-guide/security-compliance-framework.mdx
index 422dda2174..b75c501845 100644
--- a/docs/developer-guide/security-compliance-framework.mdx
+++ b/docs/developer-guide/security-compliance-framework.mdx
@@ -267,7 +267,7 @@ Every legacy compliance file is a JSON document with the following top-level key
| Field | Type | Required | Description |
|---|---|---|---|
| `Framework` | string | Yes | Canonical framework identifier, for example `CIS`, `NIST-800-53-Revision-5`, `ENS`, `CCC`. |
-| `Name` | string | Yes | Human-readable framework name displayed by Prowler App. |
+| `Name` | string | Yes | Human-readable framework name displayed by Prowler Cloud and Prowler Local Server. |
| `Version` | string | Yes (recommended) | Framework version, e.g. `2.0`. See [Version Handling](#version-handling). |
| `Provider` | string | Yes | Upper-cased provider identifier: `AWS`, `AZURE`, `GCP`, `KUBERNETES`, `M365`, `GITHUB`, `GOOGLEWORKSPACE`, and so on. |
| `Description` | string | Yes | Short description of the framework's scope and purpose. |
@@ -536,7 +536,7 @@ Each entry in the list is a single constraint with the following fields:
### How guardrails are evaluated
-All evaluation lives in one shared module, `prowler/lib/check/compliance_config_eval.py`, consumed by every compliance output (CSV, OCSF, and the CLI tables) and reused by the Prowler App backend so the rule is defined exactly once.
+All evaluation lives in one shared module, `prowler/lib/check/compliance_config_eval.py`, consumed by every compliance output (CSV, OCSF, and the CLI tables) and reused by the Prowler API backend so the rule is defined exactly once.
1. The applied configuration is the scan-global `audit_config` (the same mapping for every resource and region), resolved via `get_scan_audit_config()`.
2. For each requirement that declares constraints, `evaluate_config_constraints()` walks the list and returns `(is_compliant, reason)`. The requirement is compliant when **every** explicitly-set key satisfies its constraint.
@@ -659,7 +659,7 @@ uv run pytest -n auto \
tests/lib/outputs/compliance/
```
-## Version handling
+## Version Handling
Prowler matches frameworks by concatenating `Framework` and `Version`. A missing or empty `Version` collapses several frameworks to the same key and breaks CLI filtering with `--compliance`.
@@ -737,9 +737,9 @@ Open the generated CSV and confirm:
- Every requirement has at least one row per scanned resource (when there are findings).
- Attribute values such as `Requirements_Attributes_Section` reflect the JSON content.
-### 5. Verify the framework in Prowler App
+### 5. Verify the Framework in Prowler Local Server
-Launch Prowler App locally (`docker compose up` from the repository root) and run a scan with the new compliance framework. Confirm the compliance page renders the requirements, sections, and status widgets correctly.
+Launch Prowler Local Server (`docker compose up` from the repository root) and run a scan with the new compliance framework. Confirm the compliance page renders the requirements, sections, and status widgets correctly.
## Testing
@@ -757,7 +757,7 @@ uv run pytest -n auto tests/lib/check/universal_compliance_models_test.py \
For guidance on writing Prowler SDK tests, refer to [Unit Testing](/developer-guide/unit-testing).
-## Running and listing your framework
+## Running and Listing Your Framework
Once the file is in place, the CLI auto-discovers it:
@@ -770,7 +770,7 @@ prowler --compliance --list-compliance-requirements <
For end-user-facing tutorials (recommended for high-profile frameworks), add a dedicated page under `docs/user-guide/compliance/tutorials/` and register it in the `"Compliance"` group of `docs/docs.json`. See `docs/user-guide/compliance/tutorials/threatscore.mdx` as a reference.
-## Submitting the pull request
+## Submitting the Pull Request
Before opening the pull request:
@@ -795,7 +795,7 @@ The following issues are the most common when contributing a compliance framewor
- **CSV file is missing after the scan (legacy).** The transformer class is not registered in `prowler/lib/outputs/compliance/compliance_output.py`, or `transform()` raises silently. Run the scan with `--log-level DEBUG`.
- **Findings do not roll up under a requirement.** A check listed in `Checks` either does not exist for that provider or is spelled incorrectly. Run `--list-checks | grep ` to confirm, or run the check-existence cross-check from "Validating Your Framework".
-## Reference examples
+## Reference Examples
Use the following files as templates when modeling a new contribution.
diff --git a/docs/developer-guide/services.mdx b/docs/developer-guide/services.mdx
index 9aeaf887e4..4bd9cce36a 100644
--- a/docs/developer-guide/services.mdx
+++ b/docs/developer-guide/services.mdx
@@ -10,7 +10,7 @@ First ensure that the provider you want to add the service is already created. I
## Introduction
-In Prowler, a **service** represents a specific solution or resource offered by one of the supported [Prowler Providers](/developer-guide/provider), for example, [EC2](https://aws.amazon.com/ec2/) in AWS, or [Microsoft Exchange](https://www.microsoft.com/en-us/microsoft-365/exchange/exchange-online) in M365. Services are the building blocks that allow Prowler interact directly with the various resources exposed by each provider.
+In Prowler, a **service** represents a specific solution or resource offered by one of the supported [Prowler Providers](/developer-guide/provider), for example, [EC2](https://aws.amazon.com/ec2/) in AWS, or [Microsoft Exchange](https://www.microsoft.com/en-us/microsoft-365/exchange/exchange-online) in M365. Services are the building blocks that allow Prowler to interact directly with the various resources exposed by each provider.
Each service is implemented as a class that encapsulates all the logic, data models, and API interactions required to gather and store information about that service's resources. All of this data is used by the [Prowler checks](/developer-guide/checks) to generate the security findings.
@@ -22,9 +22,9 @@ Within this folder the following files are also to be created:
- `__init__.py` (empty) – Ensures Python recognizes this folder as a package.
- `_service.py` – Contains all the logic and API calls of the service.
-- `_client_.py` – Contains the initialization of the freshly created service's class so that the checks can use it.
+- `_client.py` – Contains the initialization of the freshly created service's class so that the checks can use it.
-Once the files are create, you can check that the service has been created by running the following command: `uv run python prowler-cli.py --list-services | grep `.
+Once the files are created, you can check that the service has been created by running the following command: `uv run python prowler-cli.py --list-services | grep `.
## Service Structure and Initialisation
@@ -32,7 +32,7 @@ The Prowler's service structure is as outlined below. To initialise it, just imp
### Service Base Class
-All Prowler provider service should inherit from a common base class to avoid code duplication. This base class handles initialization and storage of functions and objects needed across services. The exact implementation depends on the provider's API requirements, but the following are the most common responsibilities:
+All Prowler provider services should inherit from a common base class to avoid code duplication. This base class handles initialization and storage of functions and objects needed across services. The exact implementation depends on the provider's API requirements, but the following are the most common responsibilities:
- Initialize/store clients to interact with the provider's API.
- Store the audit and fixer configuration.
diff --git a/docs/developer-guide/stackit-details.mdx b/docs/developer-guide/stackit-details.mdx
index a6fa847659..20942a6236 100644
--- a/docs/developer-guide/stackit-details.mdx
+++ b/docs/developer-guide/stackit-details.mdx
@@ -60,7 +60,7 @@ StackIT uses service account keys for API authentication. Service account keys a
1. **Navigate to Service Accounts**
- Go to the [StackIT Portal](https://portal.stackit.cloud/)
- Select your project
- - Click on **Service Accounts** in the left sidebar
+ - Click **Service Accounts** in the left sidebar
2. **Create or Select Service Account**
- If you don't have a service account, click **Create Service Account**
diff --git a/docs/dockerhub/README.md b/docs/dockerhub/README.md
new file mode 100644
index 0000000000..49c3983473
--- /dev/null
+++ b/docs/dockerhub/README.md
@@ -0,0 +1,123 @@
+
+
+
+
+ Prowler is the Open Cloud Security platform trusted by thousands to automate security and compliance in any cloud environment — AWS, Azure, Google Cloud, Kubernetes, M365, GitHub and more.
+
+
+---
+
+# Prowler container images
+
+All Prowler images are built from a single repository — [github.com/prowler-cloud/prowler](https://github.com/prowler-cloud/prowler) — and published together on every release.
+
+| Image | What it is | Dockerfile |
+|---|---|---|
+| [`prowlercloud/prowler`](https://hub.docker.com/r/prowlercloud/prowler) | **Prowler CLI.** Runs scans from your terminal, a CI job, a Kubernetes Job or any container platform. | [`Dockerfile`](https://github.com/prowler-cloud/prowler/blob/master/Dockerfile) |
+| [`prowlercloud/prowler-api`](https://hub.docker.com/r/prowlercloud/prowler-api) | **Prowler Local Server — API.** Django REST backend plus the Celery worker and scheduler that run scans and store results. | [`api/Dockerfile`](https://github.com/prowler-cloud/prowler/blob/master/api/Dockerfile) |
+| [`prowlercloud/prowler-ui`](https://hub.docker.com/r/prowlercloud/prowler-ui) | **Prowler Local Server — UI.** Next.js web interface for launching scans and exploring findings. | [`ui/Dockerfile`](https://github.com/prowler-cloud/prowler/blob/master/ui/Dockerfile) |
+| [`prowlercloud/prowler-mcp`](https://hub.docker.com/r/prowlercloud/prowler-mcp) | **Prowler MCP.** Gives AI assistants access to the Prowler ecosystem over the Model Context Protocol. | [`mcp_server/Dockerfile`](https://github.com/prowler-cloud/prowler/blob/master/mcp_server/Dockerfile) |
+| [`toniblyx/prowler`](https://hub.docker.com/r/toniblyx/prowler) | **Legacy home of the Prowler CLI image.** Still mirrored on every release for backwards compatibility. New deployments should use `prowlercloud/prowler`. | [`Dockerfile`](https://github.com/prowler-cloud/prowler/blob/master/Dockerfile) |
+
+All images are published for `linux/amd64` and `linux/arm64`.
+
+## Tags
+
+| Tag | Meaning |
+|---|---|
+| `stable` | Always points to the latest stable release. **Recommended for production.** |
+| `` | A specific release, e.g. `5.14.0`. Immutable. |
+| `latest` | Built from the `master` branch on every merge. Not a stable version. |
+| `` | A specific `master` commit (`prowler-api`, `prowler-ui` and `prowler-mcp` only). |
+
+`v3-*` and `v4-*` tags on `prowlercloud/prowler` are frozen historical artifacts of Prowler v3/v4 and no longer receive updates.
+
+## Other registries
+
+The Prowler CLI image is also available on AWS Public ECR: [`public.ecr.aws/prowler-cloud/prowler`](https://gallery.ecr.aws/prowler-cloud/prowler).
+
+---
+
+# Quick start
+
+## Prowler Local Server (UI + API)
+
+```console
+curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
+curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
+docker compose up -d
+```
+
+Then open http://localhost:3000 and sign up with your email and password.
+
+Full guide: [Prowler Local Server installation](https://docs.prowler.com/getting-started/installation/prowler-app)
+
+## Prowler CLI
+
+```console
+docker run -ti --rm \
+ -v /your/local/dir/prowler-output:/home/prowler/output \
+ --name prowler \
+ --env AWS_ACCESS_KEY_ID \
+ --env AWS_SECRET_ACCESS_KEY \
+ --env AWS_SESSION_TOKEN \
+ prowlercloud/prowler:stable aws
+```
+
+Swap `aws` for `azure`, `gcp`, `kubernetes`, `m365` or `github` to scan another provider. The CLI is also on PyPI: `pip install prowler`.
+
+Full guide: [Prowler CLI installation](https://docs.prowler.com/getting-started/installation/prowler-cli)
+
+## Prowler MCP
+
+```console
+# STDIO mode (for local MCP clients)
+docker run --rm -i prowlercloud/prowler-mcp
+
+# HTTP mode (for remote access)
+docker run --rm -p 8000:8000 prowlercloud/prowler-mcp \
+ --transport http --host 0.0.0.0 --port 8000
+```
+
+Full guide: [Prowler MCP installation](https://docs.prowler.com/getting-started/installation/prowler-mcp)
+
+> **Note on architecture:** if your workstation's architecture is incompatible, set `DOCKER_DEFAULT_PLATFORM=linux/amd64` or pass `--platform linux/amd64` to your Docker command.
+
+---
+
+# What Prowler covers
+
+Hundreds of built-in checks mapped to the frameworks you get audited against — CIS, NIST 800 / CSF, CISA, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, FedRAMP, RBI, AWS Well-Architected (Security Pillar), AWS FTR, ENS — plus your own custom frameworks.
+
+For live check, service, framework and category counts, see [**Prowler Hub**](https://hub.prowler.com).
+
+List what's available for any provider:
+
+```console
+prowler --list-checks
+prowler --list-services
+prowler --list-compliance
+prowler --list-categories
+```
+
+# Documentation and support
+
+- **Documentation:** [docs.prowler.com](https://docs.prowler.com/)
+- **Source:** [github.com/prowler-cloud/prowler](https://github.com/prowler-cloud/prowler)
+- **Issues:** [github.com/prowler-cloud/prowler/issues](https://github.com/prowler-cloud/prowler/issues)
+- **Community:** [Prowler Slack](https://goto.prowler.com/slack)
+- **Troubleshooting:** [docs.prowler.com/troubleshooting](https://docs.prowler.com/troubleshooting)
+
+# License
+
+Prowler is licensed under the Apache License 2.0. A copy is available at http://www.apache.org/licenses/LICENSE-2.0.
diff --git a/docs/dockerhub/prowler-logo.svg b/docs/dockerhub/prowler-logo.svg
new file mode 100644
index 0000000000..70fd7abbb9
--- /dev/null
+++ b/docs/dockerhub/prowler-logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/docs.json b/docs/docs.json
index 8c5f2d87fb..363d6ac082 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -38,61 +38,90 @@
{
"group": "Welcome",
"pages": [
- "introduction"
+ "introduction",
+ "getting-started/products/index"
]
},
{
- "group": "Prowler Cloud",
+ "group": "Prowler Products",
"pages": [
- "getting-started/products/prowler-cloud",
- "getting-started/products/prowler-cloud-pricing",
- "getting-started/products/prowler-cloud-aws-marketplace",
- "getting-started/goto/prowler-cloud",
- "getting-started/goto/prowler-api-reference"
+ {
+ "group": "Prowler Cloud",
+ "pages": [
+ "getting-started/products/prowler-cloud",
+ "getting-started/products/prowler-cloud-pricing",
+ "getting-started/products/prowler-cloud-aws-marketplace",
+ "getting-started/goto/prowler-cloud",
+ "getting-started/goto/prowler-api-reference"
+ ]
+ },
+ {
+ "group": "Prowler Lighthouse AI",
+ "pages": [
+ "getting-started/products/prowler-cloud-lighthouse"
+ ]
+ },
+ {
+ "group": "Prowler Hub",
+ "pages": [
+ "getting-started/products/prowler-hub",
+ "getting-started/goto/prowler-hub"
+ ]
+ },
+ {
+ "group": "Prowler MCP",
+ "pages": [
+ "getting-started/products/prowler-mcp",
+ "getting-started/basic-usage/prowler-mcp",
+ "getting-started/basic-usage/prowler-mcp-tools",
+ "getting-started/installation/prowler-mcp"
+ ]
+ },
+ {
+ "group": "Prowler for AI Agents",
+ "pages": [
+ "user-guide/ai-agents/index",
+ "user-guide/ai-agents/claude-code",
+ "user-guide/ai-agents/claude-desktop",
+ "user-guide/ai-agents/codex",
+ "user-guide/ai-agents/cursor",
+ "user-guide/ai-agents/vscode"
+ ]
+ },
+ {
+ "group": "Prowler for MSPs and MSSPs",
+ "pages": [
+ "getting-started/products/prowler-for-msps",
+ "user-guide/tutorials/prowler-for-msps-sign-up",
+ "user-guide/tutorials/prowler-for-msps-organization",
+ "user-guide/tutorials/prowler-for-msps-team",
+ "user-guide/tutorials/prowler-for-msps-customers",
+ "user-guide/tutorials/prowler-for-msps-billing",
+ "user-guide/tutorials/prowler-for-msps-branding"
+ ]
+ }
]
},
{
- "group": "Prowler CLI",
+ "group": "Open Source",
"pages": [
- "getting-started/products/prowler-cli",
- "getting-started/installation/prowler-cli",
- "getting-started/basic-usage/prowler-cli"
- ]
- },
- {
- "group": "Prowler App",
- "pages": [
- "getting-started/products/prowler-app",
- "getting-started/installation/prowler-app",
- "getting-started/basic-usage/prowler-app"
- ]
- },
- {
- "group": "Prowler Lighthouse AI",
- "pages": [
- "getting-started/products/prowler-cloud-lighthouse"
- ]
- },
- {
- "group": "Prowler for Claude Code",
- "pages": [
- "getting-started/products/prowler-claude-code-plugin"
- ]
- },
- {
- "group": "Prowler MCP Server",
- "pages": [
- "getting-started/products/prowler-mcp",
- "getting-started/installation/prowler-mcp",
- "getting-started/basic-usage/prowler-mcp",
- "getting-started/basic-usage/prowler-mcp-tools"
- ]
- },
- {
- "group": "Prowler Hub",
- "pages": [
- "getting-started/products/prowler-hub",
- "getting-started/goto/prowler-hub"
+ {
+ "group": "Prowler CLI",
+ "pages": [
+ "getting-started/products/prowler-cli",
+ "getting-started/installation/prowler-cli",
+ "getting-started/basic-usage/prowler-cli"
+ ]
+ },
+ {
+ "group": "Prowler Local Server",
+ "pages": [
+ "getting-started/products/prowler-app",
+ "getting-started/installation/prowler-app",
+ "getting-started/basic-usage/prowler-app"
+ ]
+ },
+ "getting-started/products/prowler-sdk"
]
},
{
@@ -111,35 +140,45 @@
"tab": "Guides",
"groups": [
{
- "group": "Prowler Cloud/App",
+ "group": "Prowler Cloud",
"pages": [
"user-guide/tutorials/prowler-app",
{
- "group": "Authentication",
+ "group": "Authentication & Access",
"pages": [
+ "user-guide/tutorials/prowler-app-api-keys",
+ "user-guide/tutorials/prowler-app-multi-tenant",
+ "user-guide/tutorials/prowler-app-sso",
"user-guide/tutorials/prowler-app-social-login",
- "user-guide/tutorials/prowler-app-sso"
+ "user-guide/tutorials/prowler-app-rbac"
]
},
- "user-guide/tutorials/prowler-app-rbac",
- "user-guide/tutorials/prowler-app-multi-tenant",
- "user-guide/tutorials/prowler-app-api-keys",
- "user-guide/tutorials/prowler-import-findings",
- "user-guide/tutorials/prowler-scan-scheduling",
- "user-guide/tutorials/prowler-alerts",
- "user-guide/tutorials/prowler-app-scan-configuration",
- "user-guide/tutorials/prowler-app-findings-triage",
{
- "group": "Mutelist",
- "expanded": true,
+ "group": "Attack Paths",
"pages": [
- "user-guide/tutorials/prowler-app-simple-mutelist",
- "user-guide/tutorials/prowler-app-mute-findings"
+ "user-guide/tutorials/prowler-app-attack-paths",
+ "user-guide/tutorials/prowler-app-attack-paths-active-queries"
+ ]
+ },
+ {
+ "group": "Compliance",
+ "pages": [
+ "user-guide/compliance/tutorials/compliance",
+ "user-guide/compliance/tutorials/cross-provider-type-compliance",
+ "user-guide/compliance/tutorials/cross-provider-compliance",
+ "user-guide/compliance/tutorials/threatscore"
+ ]
+ },
+ {
+ "group": "Findings",
+ "pages": [
+ "user-guide/tutorials/prowler-alerts",
+ "user-guide/tutorials/prowler-app-finding-groups",
+ "user-guide/tutorials/prowler-app-findings-triage"
]
},
{
"group": "Integrations",
- "expanded": true,
"pages": [
"user-guide/tutorials/prowler-app-s3-integration",
"user-guide/tutorials/prowler-app-security-hub-integration",
@@ -147,40 +186,80 @@
]
},
{
- "group": "AWS Organizations",
- "expanded": true,
+ "group": "Mutelist",
"pages": [
- "user-guide/tutorials/prowler-cloud-aws-organizations"
+ "user-guide/tutorials/prowler-app-mute-findings",
+ "user-guide/tutorials/prowler-app-simple-mutelist"
]
},
{
- "group": "Lighthouse AI (Prowler Cloud)",
+ "group": "Providers",
+ "pages": [
+ "user-guide/tutorials/prowler-cloud-aws-organizations",
+ "user-guide/tutorials/prowler-cloud-azure-management-groups",
+ "user-guide/tutorials/prowler-cloud-gcp-organizations"
+ ]
+ },
+ {
+ "group": "Scans",
+ "pages": [
+ "user-guide/tutorials/prowler-app-scan-configuration",
+ "user-guide/tutorials/prowler-import-findings",
+ "user-guide/tutorials/prowler-scan-scheduling"
+ ]
+ },
+ {
+ "group": "Tutorials",
+ "pages": [
+ "user-guide/tutorials/aws-organizations-bulk-provisioning",
+ "user-guide/tutorials/bulk-provider-provisioning",
+ "user-guide/tutorials/prowler-app-sso-entra",
+ "user-guide/tutorials/prowler-app-sso-google-workspace"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Prowler for MSPs and MSSPs",
+ "pages": [
+ "user-guide/tutorials/prowler-for-msps-sign-up",
+ "user-guide/tutorials/prowler-for-msps-organization",
+ "user-guide/tutorials/prowler-for-msps-team",
+ "user-guide/tutorials/prowler-for-msps-customers",
+ "user-guide/tutorials/prowler-for-msps-billing",
+ "user-guide/tutorials/prowler-for-msps-branding"
+ ]
+ },
+ {
+ "group": "Prowler Lighthouse AI",
+ "pages": [
+ {
+ "group": "Prowler Cloud",
"pages": [
"user-guide/tutorials/prowler-cloud-lighthouse-multi-llm"
]
},
{
- "group": "Lighthouse AI (Open Source)",
+ "group": "Prowler Local Server",
"pages": [
"getting-started/products/prowler-lighthouse-ai",
"user-guide/tutorials/prowler-app-lighthouse",
"user-guide/tutorials/prowler-app-lighthouse-multi-llm"
]
- },
- "user-guide/tutorials/prowler-app-attack-paths",
- "user-guide/tutorials/prowler-app-finding-groups",
- "user-guide/tutorials/prowler-cloud-public-ips",
- {
- "group": "Tutorials",
- "pages": [
- "user-guide/tutorials/prowler-app-sso-entra",
- "user-guide/tutorials/prowler-app-sso-google-workspace",
- "user-guide/tutorials/bulk-provider-provisioning",
- "user-guide/tutorials/aws-organizations-bulk-provisioning"
- ]
}
]
},
+ {
+ "group": "Prowler for AI Agents",
+ "pages": [
+ "user-guide/ai-agents/index",
+ "user-guide/ai-agents/claude-code",
+ "user-guide/ai-agents/claude-desktop",
+ "user-guide/ai-agents/codex",
+ "user-guide/ai-agents/cursor",
+ "user-guide/ai-agents/vscode"
+ ]
+ },
{
"group": "CI/CD",
"pages": [
@@ -222,6 +301,13 @@
{
"group": "Providers",
"pages": [
+ {
+ "group": "Alibaba Cloud",
+ "pages": [
+ "user-guide/providers/alibabacloud/getting-started-alibabacloud",
+ "user-guide/providers/alibabacloud/authentication"
+ ]
+ },
{
"group": "AWS",
"pages": [
@@ -249,6 +335,27 @@
"user-guide/providers/azure/create-prowler-service-principal"
]
},
+ {
+ "group": "Cloudflare",
+ "pages": [
+ "user-guide/providers/cloudflare/getting-started-cloudflare",
+ "user-guide/providers/cloudflare/authentication"
+ ]
+ },
+ {
+ "group": "E2E Networks",
+ "pages": [
+ "user-guide/providers/e2enetworks/getting-started-e2enetworks",
+ "user-guide/providers/e2enetworks/authentication"
+ ]
+ },
+ {
+ "group": "GitHub",
+ "pages": [
+ "user-guide/providers/github/getting-started-github",
+ "user-guide/providers/github/authentication"
+ ]
+ },
{
"group": "Google Cloud",
"pages": [
@@ -260,10 +367,31 @@
]
},
{
- "group": "Alibaba Cloud",
+ "group": "Google Workspace",
"pages": [
- "user-guide/providers/alibabacloud/getting-started-alibabacloud",
- "user-guide/providers/alibabacloud/authentication"
+ "user-guide/providers/googleworkspace/getting-started-googleworkspace",
+ "user-guide/providers/googleworkspace/authentication"
+ ]
+ },
+ {
+ "group": "Huawei Cloud",
+ "pages": [
+ "user-guide/providers/huaweicloud/getting-started-huaweicloud",
+ "user-guide/providers/huaweicloud/authentication"
+ ]
+ },
+ {
+ "group": "IaC",
+ "pages": [
+ "user-guide/providers/iac/getting-started-iac",
+ "user-guide/providers/iac/authentication"
+ ]
+ },
+ {
+ "group": "Image",
+ "pages": [
+ "user-guide/providers/image/getting-started-image",
+ "user-guide/providers/image/authentication"
]
},
{
@@ -273,6 +401,19 @@
"user-guide/providers/kubernetes/misc"
]
},
+ {
+ "group": "Linode",
+ "pages": [
+ "user-guide/providers/linode/getting-started-linode",
+ "user-guide/providers/linode/authentication"
+ ]
+ },
+ {
+ "group": "LLM",
+ "pages": [
+ "user-guide/providers/llm/getting-started-llm"
+ ]
+ },
{
"group": "Microsoft 365",
"pages": [
@@ -281,27 +422,6 @@
"user-guide/providers/microsoft365/use-of-powershell"
]
},
- {
- "group": "Google Workspace",
- "pages": [
- "user-guide/providers/googleworkspace/getting-started-googleworkspace",
- "user-guide/providers/googleworkspace/authentication"
- ]
- },
- {
- "group": "GitHub",
- "pages": [
- "user-guide/providers/github/getting-started-github",
- "user-guide/providers/github/authentication"
- ]
- },
- {
- "group": "IaC",
- "pages": [
- "user-guide/providers/iac/getting-started-iac",
- "user-guide/providers/iac/authentication"
- ]
- },
{
"group": "MongoDB Atlas",
"pages": [
@@ -310,30 +430,11 @@
]
},
{
- "group": "Cloudflare",
+ "group": "Okta",
"pages": [
- "user-guide/providers/cloudflare/getting-started-cloudflare",
- "user-guide/providers/cloudflare/authentication"
- ]
- },
- {
- "group": "Image",
- "pages": [
- "user-guide/providers/image/getting-started-image",
- "user-guide/providers/image/authentication"
- ]
- },
- {
- "group": "LLM",
- "pages": [
- "user-guide/providers/llm/getting-started-llm"
- ]
- },
- {
- "group": "Oracle Cloud Infrastructure",
- "pages": [
- "user-guide/providers/oci/getting-started-oci",
- "user-guide/providers/oci/authentication"
+ "user-guide/providers/okta/getting-started-okta",
+ "user-guide/providers/okta/authentication",
+ "user-guide/providers/okta/retry-configuration"
]
},
{
@@ -343,6 +444,13 @@
"user-guide/providers/openstack/authentication"
]
},
+ {
+ "group": "Oracle Cloud Infrastructure",
+ "pages": [
+ "user-guide/providers/oci/getting-started-oci",
+ "user-guide/providers/oci/authentication"
+ ]
+ },
{
"group": "Scaleway",
"pages": [
@@ -363,38 +471,9 @@
"user-guide/providers/vercel/getting-started-vercel",
"user-guide/providers/vercel/authentication"
]
- },
- {
- "group": "Okta",
- "pages": [
- "user-guide/providers/okta/getting-started-okta",
- "user-guide/providers/okta/authentication",
- "user-guide/providers/okta/retry-configuration"
- ]
- },
- {
- "group": "Linode",
- "pages": [
- "user-guide/providers/linode/getting-started-linode",
- "user-guide/providers/linode/authentication"
- ]
- },
- {
- "group": "E2E Networks",
- "pages": [
- "user-guide/providers/e2enetworks/getting-started-e2enetworks",
- "user-guide/providers/e2enetworks/authentication"
- ]
}
]
},
- {
- "group": "Compliance",
- "pages": [
- "user-guide/compliance/tutorials/compliance",
- "user-guide/compliance/tutorials/threatscore"
- ]
- },
{
"group": "Cookbooks",
"pages": [
@@ -423,7 +502,8 @@
"developer-guide/mcp-server",
"developer-guide/ai-skills",
"developer-guide/prowler-studio",
- "developer-guide/server-sent-events"
+ "developer-guide/server-sent-events",
+ "developer-guide/attack-paths-queries"
]
},
{
@@ -493,19 +573,16 @@
"troubleshooting"
]
},
+ {
+ "tab": "Changelog",
+ "pages": [
+ "changelog"
+ ]
+ },
{
"tab": "About Us",
"icon": "/favicon.ico",
"href": "https://prowler.com/about#team"
- },
- {
- "tab": "Changelog",
- "icon": "github",
- "href": "https://github.com/prowler-cloud/prowler/releases"
- },
- {
- "tab": "Public Roadmap",
- "href": "https://roadmap.prowler.com/"
}
],
"global": {
@@ -541,6 +618,13 @@
}
]
},
+ "banner": {
+ "content": "Prowler App is now Prowler Local Server, and Prowler Enterprise is now Prowler Private Cloud. See [Prowler product families](/getting-started/products). Check the [latest changes](/changelog).",
+ "dismissible": false
+ },
+ "markdown": {
+ "instructions": "Prowler product naming: Prowler App is now Prowler Local Server, and Prowler Enterprise is now Prowler Private Cloud. Always use the current names when answering. The full product reference is at /getting-started/products: Open Source projects are Prowler CLI, Prowler Local Server, Prowler Local Dashboard, and Prowler SDK; Prowler Products are Prowler Cloud, Prowler Private Cloud, Prowler Hub, Prowler Lighthouse AI, and Prowler MCP."
+ },
"analytics": {
"ga4": {
"measurementId": "G-KBKV70W5Y2"
@@ -621,6 +705,14 @@
{
"source": "/user-guide/tutorials/prowler-app-alerts",
"destination": "/user-guide/tutorials/prowler-alerts"
+ },
+ {
+ "source": "/user-guide/tutorials/prowler-cloud-public-ips",
+ "destination": "/security/networking"
+ },
+ {
+ "source": "/getting-started/products/prowler-claude-code-plugin",
+ "destination": "/user-guide/ai-agents/claude-code"
}
]
}
diff --git a/docs/getting-started/basic-usage/prowler-app.mdx b/docs/getting-started/basic-usage/prowler-app.mdx
index 2e66357270..80ea1e3292 100644
--- a/docs/getting-started/basic-usage/prowler-app.mdx
+++ b/docs/getting-started/basic-usage/prowler-app.mdx
@@ -3,7 +3,7 @@ title: 'Get started with the Prowler App web interface'
description: 'Sign up, add a cloud provider, launch a scan, and review findings in the Prowler App web UI for AWS, Azure, GCP, Kubernetes, and Microsoft 365.'
---
-## Access Prowler App
+## Access Prowler Local Server
After [installation](/getting-started/installation/prowler-app), navigate to [http://localhost:3000](http://localhost:3000) and sign up with email and password.
@@ -29,7 +29,7 @@ This mechanism ensures that the first user in a newly created tenant has adminis
## Log In
-Access Prowler App by logging in with **email and password**.
+Access Prowler Local Server by logging in with **email and password**.
@@ -63,7 +63,7 @@ Review findings during scan execution in the following sections:
- **Compliance** – Displays compliance insights based on security frameworks.
-> For detailed usage instructions, refer to the [Prowler App Guide](/user-guide/tutorials/prowler-app).
+> For detailed usage instructions, refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app), which also applies to Prowler Local Server.
Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored.
diff --git a/docs/getting-started/basic-usage/prowler-cli.mdx b/docs/getting-started/basic-usage/prowler-cli.mdx
index 28859bc9ac..51d2b88c48 100644
--- a/docs/getting-started/basic-usage/prowler-cli.mdx
+++ b/docs/getting-started/basic-usage/prowler-cli.mdx
@@ -17,7 +17,7 @@ prowler

-Running the `prowler` command without options will uses environment variable credentials. Refer to the Authentication section of each provider for credential configuration details.
+Running the `prowler` command without options will use environment variable credentials. Refer to the Authentication section of each provider for credential configuration details.
## Verbose Output
@@ -185,7 +185,7 @@ Prowler enables security scanning of Kubernetes clusters, supporting both **in-c
```
- By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
+ By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context` flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
## Microsoft 365
diff --git a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx
index 9d997e58ab..3be229bcf8 100644
--- a/docs/getting-started/basic-usage/prowler-mcp-tools.mdx
+++ b/docs/getting-started/basic-usage/prowler-mcp-tools.mdx
@@ -7,11 +7,16 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool
## Tool Categories Summary
-| Category | Tool Count | Authentication Required |
-|----------|------------|------------------------|
-| Prowler Hub | 10 tools | No |
-| Prowler Documentation | 2 tools | No |
-| Prowler Cloud/App | 32 tools | Yes |
+| Category | Tool Count | Authentication Required | Availability |
+|----------|------------|------------------------|--------------|
+| Prowler Hub | 10 tools | No | Cloud and Local MCP Server |
+| Prowler Documentation | 2 tools | No | Cloud and Local MCP Server |
+| Prowler Cloud, Private Cloud & Local Server | 49 tools | Yes | Cloud and Local MCP Server |
+| Prowler Cloud management | 32 tools | Yes | Cloud MCP Server only |
+
+
+48 of the 49 Prowler tools are available on both servers. `prowler_schedule_daily_scan` is the exception: it is Local-only, because the Cloud MCP Server supersedes it with the `prowler_cloud_*` [Scan Scheduling](#scan-scheduling) tools.
+
## Tool Naming Convention
@@ -19,11 +24,16 @@ All tools follow a consistent naming pattern with prefixes:
- `prowler_hub_*` - Prowler Hub catalog and compliance tools
- `prowler_docs_*` - Prowler documentation search and retrieval
-- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
+- `prowler_*` - Prowler Cloud, Prowler Private Cloud & Prowler Local Server management tools
+- `prowler_cloud_*` - Prowler Cloud-only management tools
-## Prowler Cloud/App Tools
+
+`prowler_cloud_*` tools are exposed only by the [Cloud MCP Server](/getting-started/products/prowler-mcp#cloud-vs-local-mcp-server) at `https://mcp.prowler.com/mcp`, because they manage features that exist only in Prowler Cloud. Every other tool is available on both the Cloud and Local MCP Server.
+
-Manage Prowler Cloud or Prowler App (Self-Managed) features. **Requires authentication.**
+## Prowler Tools
+
+Manage your Prowler deployment — Prowler Cloud, Prowler Private Cloud, or Prowler Local Server. **Requires authentication.**
These tools require a valid API key. See the [Configuration Guide](/getting-started/basic-usage/prowler-mcp) for authentication setup.
@@ -33,44 +43,48 @@ These tools require a valid API key. See the [Configuration Guide](/getting-star
Tools for searching, viewing, and analyzing security findings across all cloud providers.
-- **`prowler_app_search_security_findings`** - Search and filter security findings with advanced filtering options (severity, status, provider, region, service, check ID, date range, muted status)
-- **`prowler_app_get_finding_details`** - Get comprehensive details about a specific finding including remediation guidance, check metadata, and resource relationships
-- **`prowler_app_get_findings_overview`** - Get aggregate statistics and trends about security findings as a markdown report
+- **`prowler_search_security_findings`** - Search and filter security findings with advanced filtering options (severity, status, provider, region, service, check ID, date range, muted status)
+- **`prowler_get_finding_details`** - Get comprehensive details about a specific finding including remediation guidance, check metadata, and resource relationships
+- **`prowler_get_findings_overview`** - Get aggregate statistics and trends about security findings as a markdown report
### Finding Groups Management
Tools for listing finding groups aggregated by check ID, viewing complete group counters, and drilling down into affected resources.
-- **`prowler_app_list_finding_groups`** - List latest or historical finding groups with filters for provider, region, service, resource, category, check, severity, status, muted state, delta, date range, and sorting
-- **`prowler_app_get_finding_group_details`** - Get complete details for a specific finding group including counters, description, timestamps, and impacted providers
-- **`prowler_app_list_finding_group_resources`** - List actionable unmuted resources affected by a finding group by default, including nested resource and provider data plus the `finding_id` for remediation details. Set `include_muted` to include suppressed resources
+- **`prowler_list_finding_groups`** - List latest or historical finding groups with filters for provider, region, service, resource, category, check, severity, status, muted state, delta, date range, and sorting
+- **`prowler_get_finding_group_details`** - Get complete details for a specific finding group including counters, description, timestamps, and impacted providers
+- **`prowler_list_finding_group_resources`** - List actionable unmuted resources affected by a finding group by default, including nested resource and provider data plus the `finding_id` for remediation details. Set `include_muted` to include suppressed resources
### Provider Management
Tools for managing cloud provider connections in Prowler.
-- **`prowler_app_search_providers`** - Search and view configured providers with their connection status
-- **`prowler_app_connect_provider`** - Register and connect a provider with credentials for security scanning
-- **`prowler_app_delete_provider`** - Permanently remove a provider from Prowler
+- **`prowler_search_providers`** - Search and view configured providers with their connection status
+- **`prowler_connect_provider`** - Register and connect a provider with credentials for security scanning
+- **`prowler_delete_provider`** - Permanently remove a provider from Prowler
### Scan Management
Tools for managing and monitoring security scans.
-- **`prowler_app_list_scans`** - List and filter security scans across all providers
-- **`prowler_app_get_scan`** - Get comprehensive details about a specific scan (progress, duration, resource counts)
-- **`prowler_app_trigger_scan`** - Trigger a manual security scan for a provider
-- **`prowler_app_schedule_daily_scan`** - Schedule automated daily scans for continuous monitoring
-- **`prowler_app_update_scan`** - Update scan name for better organization
+- **`prowler_list_scans`** - List and filter security scans across all providers
+- **`prowler_get_scan`** - Get comprehensive details about a specific scan (progress, duration, resource counts)
+- **`prowler_trigger_scan`** - Trigger a manual security scan for a provider
+- **`prowler_schedule_daily_scan`** - Schedule automated daily scans for continuous monitoring (**Local MCP Server only**)
+- **`prowler_update_scan`** - Update scan name for better organization
+
+
+`prowler_schedule_daily_scan` is the scheduling tool for a self-hosted deployment, and it only does one thing: a daily scan. The Cloud MCP Server does not expose it — Prowler Cloud replaces it with the richer [Scan Scheduling](#scan-scheduling) tools, which add interval, weekly, and monthly frequencies, per-provider schedule retrieval, and bulk apply across providers.
+
### Resources Management
Tools for searching, viewing, and analyzing cloud resources discovered by Prowler.
-- **`prowler_app_list_resources`** - List and filter cloud resources with advanced filtering options (provider, region, service, resource type, tags)
-- **`prowler_app_get_resource`** - Get comprehensive details about a specific resource including configuration, metadata, and finding relationships
-- **`prowler_app_get_resource_events`** - Get the timeline of cloud API actions performed on a resource (AWS CloudTrail). Shows who did what and when, with full request/response payloads
-- **`prowler_app_get_resources_overview`** - Get aggregate statistics about cloud resources as a markdown report
+- **`prowler_list_resources`** - List and filter cloud resources with advanced filtering options (provider, region, service, resource type, tags)
+- **`prowler_get_resource`** - Get comprehensive details about a specific resource including configuration, metadata, and finding relationships
+- **`prowler_get_resource_events`** - Get the timeline of cloud API actions performed on a resource (AWS CloudTrail). Shows who did what and when, with full request/response payloads
+- **`prowler_get_resources_overview`** - Get aggregate statistics about cloud resources as a markdown report
### Muting Management
@@ -78,33 +92,145 @@ Tools for managing finding muting, including pattern-based bulk muting (mutelist
#### Mutelist (Pattern-Based Muting)
-- **`prowler_app_get_mutelist`** - Retrieve the current mutelist configuration for the tenant
-- **`prowler_app_set_mutelist`** - Create or update the mutelist configuration for pattern-based bulk muting
-- **`prowler_app_delete_mutelist`** - Remove the mutelist configuration from the tenant
+- **`prowler_get_mutelist`** - Retrieve the current mutelist configuration for the tenant
+- **`prowler_set_mutelist`** - Create or update the mutelist configuration for pattern-based bulk muting
+- **`prowler_delete_mutelist`** - Remove the mutelist configuration from the tenant
#### Mute Rules (Finding-Specific Muting)
-- **`prowler_app_list_mute_rules`** - Search and filter mute rules with pagination support
-- **`prowler_app_get_mute_rule`** - Retrieve comprehensive details about a specific mute rule
-- **`prowler_app_create_mute_rule`** - Create a new mute rule to mute specific findings with documentation and audit trail
-- **`prowler_app_update_mute_rule`** - Update a mute rule's name, reason, or enabled status
-- **`prowler_app_delete_mute_rule`** - Delete a mute rule from the system
+- **`prowler_list_mute_rules`** - Search and filter mute rules with pagination support
+- **`prowler_get_mute_rule`** - Retrieve comprehensive details about a specific mute rule
+- **`prowler_create_mute_rule`** - Create a new mute rule to mute specific findings with documentation and audit trail
+- **`prowler_update_mute_rule`** - Update a mute rule's name, reason, or enabled status
+- **`prowler_delete_mute_rule`** - Delete a mute rule from the system
+
+### Integrations Management
+
+Tools for managing where Prowler sends its results: Amazon S3 buckets, AWS Security Hub, and Jira. Requires the **Manage Integrations** permission.
+
+#### Integration Lifecycle
+
+- **`prowler_list_integrations`** - List the configured integrations with their enabled and connection state, optionally filtered by integration type
+- **`prowler_get_integration`** - Get an integration with its complete, type-specific configuration (bucket and output directory, Security Hub settings and enabled regions, or Jira projects and issue types)
+- **`prowler_update_integration`** - Update credentials, configuration, attached providers, or enabled state. Configuration changes are merged with the current one, and the connection is re-checked automatically whenever credentials, configuration, or attached providers change
+- **`prowler_delete_integration`** - Permanently remove an integration and its stored credentials
+- **`prowler_test_integration_connection`** - Check an integration connection and refresh the configuration Prowler discovers from the remote system (Jira projects, Security Hub regions)
+
+#### Integration Setup
+
+- **`prowler_create_amazon_s3_integration`** - Export scan outputs (CSV, HTML, OCSF JSON, compliance reports) to an S3 bucket, using an IAM role or static credentials
+- **`prowler_create_aws_security_hub_integration`** - Send findings to AWS Security Hub in ASFF format for a single AWS provider, reusing the provider credentials or dedicated ones
+- **`prowler_create_jira_integration`** - Connect an Atlassian Jira site so findings can be turned into work items. Tenant-wide, not attached to any provider
+
+#### Jira Operations
+
+- **`prowler_get_jira_issue_types`** - List the issue types available in a Jira project, fetched live from Jira
+- **`prowler_send_findings_to_jira`** - Create one Jira work item per finding, with its severity, resource, risk, and remediation steps
### Attack Paths Analysis
Tools for analyzing privilege escalation chains and security misconfigurations using graph-based analysis. Attack Paths maps relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited.
-- **`prowler_app_list_attack_paths_scans`** - List Attack Paths scans with filtering by provider, provider type, and scan state (available, scheduled, executing, completed, failed, cancelled)
-- **`prowler_app_list_attack_paths_queries`** - Discover available Attack Paths queries for a completed scan, including query names, descriptions, and required parameters
-- **`prowler_app_run_attack_paths_query`** - Execute an Attack Paths query against a completed scan and retrieve graph results with nodes (cloud resources, findings, virtual nodes) and relationships (access paths, role assumptions, security group memberships)
-- **`prowler_app_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema (node labels, relationships, properties) for writing accurate custom openCypher queries
+- **`prowler_list_attack_paths_scans`** - List Attack Paths scans with filtering by provider, provider type, and scan state (available, scheduled, executing, completed, failed, cancelled)
+- **`prowler_list_attack_paths_queries`** - Discover available Attack Paths queries for a completed scan, including query names, descriptions, and required parameters
+- **`prowler_run_attack_paths_query`** - Execute an Attack Paths query against a completed scan and retrieve graph results with nodes (cloud resources, findings, virtual nodes) and relationships (access paths, role assumptions, security group memberships)
+- **`prowler_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema (node labels, relationships, properties) for writing accurate custom openCypher queries
### Compliance Management
Tools for viewing compliance status and framework details across all cloud providers.
-- **`prowler_app_get_compliance_overview`** - Get high-level compliance status across all frameworks for a specific scan or provider, including pass/fail statistics per framework
-- **`prowler_app_get_compliance_framework_state_details`** - Get detailed requirement-level breakdown for a specific compliance framework, including failed requirements and associated finding IDs
+- **`prowler_get_compliance_overview`** - Get high-level compliance status across all frameworks for a specific scan or provider, including pass/fail statistics per framework
+- **`prowler_get_compliance_framework_state_details`** - Get detailed requirement-level breakdown for a specific compliance framework, including failed requirements and associated finding IDs
+
+### User Management
+
+Tools for viewing the users in your tenant and identifying the authenticated user.
+
+- **`prowler_list_users`** - List the users in the tenant with their names and emails
+- **`prowler_get_user`** - Get detailed information about a specific user by ID, including join date and role/membership IDs
+- **`prowler_get_current_user`** - Identify which user the current credentials authenticate as
+
+### Role Management
+
+Tools for browsing RBAC roles and managing the role assigned to a user. A user holds exactly one role, so setting a role replaces the one they held before.
+
+- **`prowler_list_roles`** - List the roles defined in the tenant with their permission scope
+- **`prowler_get_role`** - Get detailed information about a specific role by ID, including granted capabilities, visibility scope, assigned users, and provider groups
+- **`prowler_get_user_roles`** - List the roles assigned to a specific user, with the capabilities each role grants
+- **`prowler_set_user_role`** - Set the role a user holds, replacing the role they had before (idempotent)
+
+## Prowler Cloud Tools
+
+Manage Prowler Cloud-only features and configuration. **Requires authentication.**
+
+
+These tools are available **only on the Cloud MCP Server** (`https://mcp.prowler.com/mcp`). A Local MCP Server does not expose them, because the features they manage exist only in Prowler Cloud.
+
+
+### Scan Configurations
+
+Tools for managing reusable scan configurations — per-provider check and compliance selections — and attaching them to providers. Providers without a configuration attached use the default.
+
+- **`prowler_cloud_list_scan_configurations`** - List and filter the scan configurations defined in the tenant
+- **`prowler_cloud_get_scan_configuration`** - Retrieve a scan configuration including its full configuration body
+- **`prowler_cloud_get_scan_configuration_schema`** - Fetch the JSON Schema describing the keys a valid configuration body may set, optionally filtered to a single provider type
+- **`prowler_cloud_create_scan_configuration`** - Create a scan configuration and optionally attach it to providers
+- **`prowler_cloud_update_scan_configuration`** - Update a configuration's name, body, and/or attached providers
+- **`prowler_cloud_delete_scan_configuration`** - Delete a scan configuration; attached providers revert to the default
+
+### Findings Triage
+
+Tools for recording a review decision on a finding and documenting the reasoning. Triage is keyed on the stable finding UID returned by `prowler_search_security_findings` and `prowler_get_finding_details`.
+
+
+Triage is distinct from [muting](#muting-management). Use mute rules and the mutelist to **suppress** findings; use triage to **record a decision** and its rationale while the finding stays visible. See the [Findings Triage tutorial](/user-guide/tutorials/prowler-app-findings-triage).
+
+
+- **`prowler_cloud_list_finding_triages`** - List and filter persisted triage records by status, provider, check, and more
+- **`prowler_cloud_get_finding_triage`** - Retrieve a single finding's triage state by finding UID
+- **`prowler_cloud_set_finding_triage_status`** - Set a finding's triage status (`open`, `under_review`, `remediating`, `risk_accepted`, `false_positive`), optionally attaching a note. The `resolved` and `reopened` statuses are system-managed and cannot be set directly
+- **`prowler_cloud_list_finding_triage_notes`** - List the notes attached to a finding's triage, newest first
+- **`prowler_cloud_create_finding_triage_note`** - Add a new note to a finding's triage
+- **`prowler_cloud_update_finding_triage_note`** - Update the body of an existing note
+- **`prowler_cloud_delete_finding_triage_note`** - Delete a note from a finding's triage
+
+### Scan Scheduling
+
+Tools for configuring recurring scans. One schedule exists per provider, with daily, interval, weekly, or monthly frequency. These replace the Local-only `prowler_schedule_daily_scan`, which can only set up a daily scan. See the [Scan Scheduling tutorial](/user-guide/tutorials/prowler-scan-scheduling).
+
+- **`prowler_cloud_list_scan_schedules`** - List scan schedules, one per visible provider
+- **`prowler_cloud_get_scan_schedule`** - Retrieve a provider's schedule including all per-frequency fields
+- **`prowler_cloud_set_scan_schedule`** - Configure or update a single provider's recurring scan schedule
+- **`prowler_cloud_bulk_set_scan_schedules`** - Apply one schedule to many providers at once
+- **`prowler_cloud_delete_scan_schedule`** - Delete a provider's scan schedule
+
+### Alerts
+
+Tools for notifying recipients when scan results match a rule condition. See the [Alerts tutorial](/user-guide/tutorials/prowler-alerts).
+
+#### Alert Rules
+
+- **`prowler_cloud_list_alert_rules`** - List and filter the custom alert rules defined in the tenant
+- **`prowler_cloud_get_alert_rule`** - Retrieve an alert rule including its condition DSL and recipient emails
+- **`prowler_cloud_create_alert_rule`** - Create a tenant-scoped alert rule
+- **`prowler_cloud_update_alert_rule`** - Update an alert rule; only the fields provided change
+- **`prowler_cloud_delete_alert_rule`** - Delete an alert rule
+- **`prowler_cloud_list_alert_rule_events`** - List the fired-alert history for a single rule, newest first
+- **`prowler_cloud_build_alert_rule_condition`** - Build a condition from a findings filter and dry-run it in one call to preview what would match. Nothing is persisted
+
+#### Alert Recipients
+
+- **`prowler_cloud_list_alert_recipients`** - List alert recipients with their confirmation status
+- **`prowler_cloud_get_alert_recipient`** - Retrieve a single recipient with its confirmation status
+- **`prowler_cloud_create_alert_recipient`** - Register a new recipient email
+- **`prowler_cloud_resend_alert_recipient_confirmation`** - Re-send the confirmation email to a pending or unsubscribed recipient
+- **`prowler_cloud_delete_alert_recipient`** - Delete an alert recipient
+
+#### Alert Events
+
+- **`prowler_cloud_list_alert_events`** - List the fired alert events for the tenant
+- **`prowler_cloud_get_alert_event`** - Retrieve a single alert event including its matched rule and scan
## Prowler Hub Tools
@@ -146,7 +272,8 @@ Search and access official Prowler documentation. **No authentication required.*
- Use natural language to interact with the tools through your AI assistant
- Tools can be combined for complex workflows
- Filter options are available on most list tools
-- Authentication is only required for Prowler Cloud/App tools
+- Authentication is only required for the `prowler_*` and `prowler_cloud_*` tools; Prowler Hub and Prowler Documentation tools work without a key
+- If a `prowler_cloud_*` tool is missing from your client, you are connected to a Local MCP Server — point it at `https://mcp.prowler.com/mcp` instead
## Additional Resources
diff --git a/docs/getting-started/basic-usage/prowler-mcp.mdx b/docs/getting-started/basic-usage/prowler-mcp.mdx
index 2625b235ab..20ae546214 100644
--- a/docs/getting-started/basic-usage/prowler-mcp.mdx
+++ b/docs/getting-started/basic-usage/prowler-mcp.mdx
@@ -8,10 +8,10 @@ Configure your MCP client to connect to Prowler MCP Server.
## Step 1: Get Your API Key
-**Authentication is optional**: Prowler Hub and Prowler Documentation features work without authentication. An API key is only required for Prowler Cloud and Prowler App (Self-Managed) features.
+**Authentication is optional**: Prowler Hub and Prowler Documentation features work without authentication. An API key is only required for Prowler tools (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server).
-To use Prowler Cloud or Prowler App (Self-Managed) features. To get the API key, please refer to the [API Keys](/user-guide/tutorials/prowler-app-api-keys) guide.
+An API key authenticates the Prowler tools (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server). To get the API key, please refer to the [API Keys](/user-guide/tutorials/prowler-app-api-keys) guide.
Keep the API key secure. Never share it publicly or commit it to version control.
@@ -19,12 +19,36 @@ Keep the API key secure. Never share it publicly or commit it to version control
## Step 2: Configure Your MCP Host/Client
-Choose the configuration based on your deployment:
+Most users should use the **Cloud MCP Server** — it needs no installation and is maintained by Prowler. The [Local MCP Server](#local-mcp-server-configuration) configuration is provided afterwards for users who run the server themselves.
-- **HTTP Mode**: Prowler Cloud MCP Server or self-hosted Prowler MCP Server.
-- **STDIO Mode**: Local installation only (runs as subprocess of your MCP client).
+- **Cloud MCP Server (HTTP)**: the managed server at `https://mcp.prowler.com/mcp` (or your own self-hosted HTTP server).
+- **Local MCP Server (STDIO)**: local installation only (runs as a subprocess of your MCP client).
-### HTTP Mode
+### Step-by-Step Guides Per Agent
+
+The tabs below are a quick configuration reference. For a walkthrough with screenshots, troubleshooting, and client-specific caveats, follow the dedicated guide for your agent:
+
+
+
+ Plugin vs. MCP-only, and which Claude surfaces work
+
+
+ The Chat tab, via a local bridge
+
+
+ CLI and the VS Code extension
+
+
+ Global and project scopes
+
+
+ Agent mode with secure key prompts
+
+
+
+## Cloud MCP Server Configuration (Recommended)
+
+Connect to the **Cloud MCP Server** at `https://mcp.prowler.com/mcp` over HTTP. This is the recommended path — no installation, always up to date. The same configuration works for a self-hosted HTTP server: just swap the URL.
@@ -62,10 +86,10 @@ Choose the configuration based on your deployment:
"args": [
"https://mcp.prowler.com/mcp", // or your self-hosted Prowler MCP Server URL
"--header",
- "Authorization: Bearer ${PROWLER_APP_API_KEY}"
+ "Authorization: Bearer ${PROWLER_API_KEY}"
],
"env": {
- "PROWLER_APP_API_KEY": ""
+ "PROWLER_API_KEY": ""
}
}
}
@@ -75,72 +99,11 @@ Choose the configuration based on your deployment:
The `mcp-remote` tool acts as a bridge for clients that don't support HTTP natively. Learn more at [mcp-remote on npm](https://www.npmjs.com/package/mcp-remote).
-
-
- 1. Open Claude Desktop settings
- 2. Go to "Developer" tab
- 3. Click in "Edit Config" button
- 4. Edit the `claude_desktop_config.json` file with your favorite editor
- 5. Install a reviewed version of `mcp-remote` in a dedicated local workspace:
- ```bash
- mkdir -p ~/.local/share/prowler-mcp-bridge
- cd ~/.local/share/prowler-mcp-bridge
- npm init -y
- npm install --save-exact mcp-remote@0.1.38
- ```
- 6. Add the following configuration:
- ```json
- {
- "mcpServers": {
- "prowler": {
- "command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote",
- "args": [
- "https://mcp.prowler.com/mcp",
- "--header",
- "Authorization: Bearer ${PROWLER_APP_API_KEY}"
- ],
- "env": {
- "PROWLER_APP_API_KEY": ""
- }
- }
- }
- }
- ```
-
-
-
- Run the following command:
- ```bash
- export PROWLER_APP_API_KEY=""
- claude mcp add --transport http prowler https://mcp.prowler.com/mcp --header "Authorization: Bearer $PROWLER_APP_API_KEY" --scope user
- ```
-
-
-
- 1. Open Cursor settings
- 2. Go to "Tools & MCP"
- 3. Click in "New MCP Server" button
- 4. Add to the JSON Configuration the following:
- ```json
- {
- "mcpServers": {
- "prowler": {
- "url": "https://mcp.prowler.com/mcp",
- "headers": {
- "Authorization": "Bearer "
- }
- }
- }
- }
- ```
-
-
-
-### STDIO Mode
+## Local MCP Server Configuration
-STDIO mode is only available when running the MCP server locally.
+STDIO mode is only available when running the **Local MCP Server** on your own machine. See the [Installation guide](/getting-started/installation/prowler-mcp) to set it up first.
@@ -153,7 +116,7 @@ STDIO mode is only available when running the MCP server locally.
"command": "uvx",
"args": ["/absolute/path/to/prowler/mcp_server/"],
"env": {
- "PROWLER_APP_API_KEY": "",
+ "PROWLER_API_KEY": "",
"API_BASE_URL": "https://api.prowler.com/api/v1"
}
}
@@ -180,7 +143,7 @@ STDIO mode is only available when running the MCP server locally.
"--rm",
"-i",
"--env",
- "PROWLER_APP_API_KEY=",
+ "PROWLER_API_KEY=",
"--env",
"API_BASE_URL=https://api.prowler.com/api/v1",
"prowlercloud/prowler-mcp"
@@ -206,7 +169,7 @@ Restart your MCP client and start asking questions:
## Authentication Methods
-Prowler MCP Server supports two authentication methods to connect to Prowler Cloud or Prowler App (Self-Managed):
+Prowler MCP Server supports two authentication methods to connect to Prowler (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server):
### API Key (Recommended)
diff --git a/docs/getting-started/comparison/microsoftdefender.mdx b/docs/getting-started/comparison/microsoftdefender.mdx
index 7c37788dc5..057e474bc8 100644
--- a/docs/getting-started/comparison/microsoftdefender.mdx
+++ b/docs/getting-started/comparison/microsoftdefender.mdx
@@ -7,7 +7,7 @@ description: 'Use Prowler open-source scans alongside Microsoft Defender for Clo
---
-## **Overview**
+## Overview
If you're using Microsoft Defender for Cloud to monitor your Azure infrastructure, Prowler can complement it with fully transparent, customizable scans across Azure, AWS, GCP, and Kubernetes. Prowler helps you validate policies, automate compliance, and gain deeper visibility—all from the CLI, API or our Prowler UI.
@@ -20,7 +20,7 @@ You can run Prowler alongside Defender for Cloud to:
---
-## **Why use Prowler with Defender for Cloud**
+## Why Use Prowler with Defender for Cloud
Microsoft Defender for Cloud offers centralized dashboards, alerting, and some cross-cloud coverage. Prowler provides full transparency and control over what’s being checked and how those checks work—no vendor lock-in, no surprises.
@@ -33,11 +33,11 @@ Use them together to get:
---
-## **Quickstart**
+## Quickstart
Here’s how to install Prowler and run a scan in your Azure account.
-### **1\. Install Prowler**
+### 1\. Install Prowler
```
git clone https://github.com/prowler-cloud/prowler
@@ -45,7 +45,7 @@ cd prowler
./install.sh
```
-### **2\. Authenticate with Azure**
+### 2\. Authenticate with Azure
Make sure you're signed in and select your subscription:
@@ -54,7 +54,7 @@ az login
export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
```
-### **3\. Run a scan**
+### 3\. Run a Scan
```
./prowler -p Azure -f az-aks -f az-general
@@ -62,7 +62,7 @@ export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
This will run checks focused on Azure Kubernetes Service (AKS) and general Azure best practices.
-### **4\. Review results**
+### 4\. Review Results
```
cat output/prowler-output-*.json
@@ -73,7 +73,7 @@ You can export findings in JSON, CSV, JUnit, HTML, or AWS Security Hub–compati
---
-## **Compare capabilities**
+## Compare Capabilities
| Feature | Microsoft Defender for Cloud | Prowler |
| ----- | ----- | ----- |
@@ -87,7 +87,7 @@ You can export findings in JSON, CSV, JUnit, HTML, or AWS Security Hub–compati
---
-## **Common use cases**
+## Common Use Cases
**✅ Validate policies**
Run Prowler to confirm your Azure policies are configured as expected and compliant with frameworks like CIS or NIST.
diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx
index 0b66f1dc79..0744fafc6d 100644
--- a/docs/getting-started/installation/prowler-app.mdx
+++ b/docs/getting-started/installation/prowler-app.mdx
@@ -5,9 +5,9 @@ description: 'Install the self-hosted Prowler App with Docker Compose or from so
### Installation
-Prowler App offers flexible installation methods tailored to various environments.
+Prowler Local Server offers flexible installation methods tailored to various environments.
-Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detailed usage instructions.
+Refer to the [Prowler Cloud guide](/user-guide/tutorials/prowler-app) for detailed usage instructions.
Prowler configuration is based on `.env` files. Every version of Prowler can have differences on that file, so, please, use the file that corresponds with that version or repository branch or tag.
@@ -110,17 +110,17 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
pnpm start
```
- > Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
+ > Enjoy Prowler Local Server at http://localhost:3000 by signing up with your email and password.
- Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
+ Google and GitHub authentication works out of the box in [Prowler Cloud](https://prowler.com). In Prowler Local Server it requires OAuth credentials: see [Social Login Configuration](/user-guide/tutorials/prowler-app-social-login).
-### Updating Prowler App
+### Updating Prowler Local Server
-Upgrade Prowler App installation using one of two options:
+Upgrade Prowler Local Server installation using one of two options:
#### Option 1: Updating the Environment File
@@ -129,12 +129,12 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
-PROWLER_UI_VERSION="5.33.0"
-PROWLER_API_VERSION="5.33.0"
+PROWLER_UI_VERSION="5.38.0"
+PROWLER_API_VERSION="5.38.0"
```
- You can find the latest versions of Prowler App in the [Releases Github section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
+ You can find the latest versions of Prowler Local Server in the [Releases Github section](https://github.com/prowler-cloud/prowler/releases) or in the [Container Versions](#container-versions) section of this documentation.
@@ -173,18 +173,18 @@ docker compose up -d
### Container Versions
-The available versions of Prowler App are the following:
+The available versions of Prowler Local Server are the following:
- `latest`: in sync with `master` branch (please note that it is not a stable version)
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
- `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
-- `stable`: this tag always point to the latest release.
-- `v4-stable`: this tag always point to the latest release for v4.
-- `v3-stable`: this tag always point to the latest release for v3.
+- `stable`: this tag always points to the latest release.
+- `v4-stable`: this tag always points to the latest release for v4.
+- `v3-stable`: this tag always points to the latest release for v3.
The container images are available here:
-- Prowler App:
+- Prowler Local Server:
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
diff --git a/docs/getting-started/installation/prowler-cli.mdx b/docs/getting-started/installation/prowler-cli.mdx
index 0e9c177405..06ea7c4b7f 100644
--- a/docs/getting-started/installation/prowler-cli.mdx
+++ b/docs/getting-started/installation/prowler-cli.mdx
@@ -235,9 +235,9 @@ The available versions of Prowler CLI are the following:
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
- `` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
-- `stable`: this tag always point to the latest release.
-- `v4-stable`: this tag always point to the latest release for v4.
-- `v3-stable`: this tag always point to the latest release for v3.
+- `stable`: this tag always points to the latest release.
+- `v4-stable`: this tag always points to the latest release for v4.
+- `v3-stable`: this tag always points to the latest release for v3.
The container images are available here:
diff --git a/docs/getting-started/installation/prowler-mcp.mdx b/docs/getting-started/installation/prowler-mcp.mdx
index 8cf3d7f7a7..18ee096090 100644
--- a/docs/getting-started/installation/prowler-mcp.mdx
+++ b/docs/getting-started/installation/prowler-mcp.mdx
@@ -6,12 +6,12 @@ description: 'Run the Prowler MCP Server locally using Docker, PyPI, or source w
There are **two ways** to use Prowler MCP Server:
-
+
**No installation required** - Just configuration
Use `https://mcp.prowler.com/mcp`
-
+
**Local installation** - Full control
Install via Docker, PyPI, or source code
@@ -19,8 +19,8 @@ There are **two ways** to use Prowler MCP Server:
-For "Option 1: Managed by Prowler", go directly to the [Configuration Guide](/getting-started/basic-usage/prowler-mcp#hosted-server-configuration-recommended) to set up your Claude Desktop, Cursor, or other MCP client.
-**This guide is focused on local installation, "Option 2: Run Locally"**.
+For the Cloud MCP Server, go directly to the [Configuration Guide](/getting-started/basic-usage/prowler-mcp#cloud-mcp-server-configuration-recommended) to set up your Claude Desktop, Cursor, or other MCP client.
+**This guide is focused on local installation, the Local MCP Server**.
## Installation Methods
@@ -52,7 +52,7 @@ Choose one of the following installation methods:
```bash
docker run --rm -i \
- -e PROWLER_APP_API_KEY="pk_your_api_key" \
+ -e PROWLER_API_KEY="pk_your_api_key" \
-e API_BASE_URL="https://api.prowler.com/api/v1" \
prowlercloud/prowler-mcp
```
@@ -144,7 +144,7 @@ Choose one of the following installation methods:
## Updating Prowler MCP Server
-When running Prowler MCP Server locally ("Option 2: Run Locally"), upgrade to the latest version using the same method chosen for installation. The hosted server (`https://mcp.prowler.com/mcp`) is always kept up to date by Prowler and requires no action.
+When running the Local MCP Server, upgrade to the latest version using the same method chosen for installation. The Cloud MCP Server (`https://mcp.prowler.com/mcp`) is always kept up to date by Prowler and requires no action.
@@ -220,19 +220,19 @@ Configure the server using environment variables:
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
-| `PROWLER_APP_API_KEY` | Prowler API key | Only for STDIO mode | - |
+| `PROWLER_API_KEY` | Prowler API key | Only for STDIO mode | - |
| `API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com/api/v1` |
| `PROWLER_MCP_TRANSPORT_MODE` | Default transport mode (overwritten by `--transport` argument) | No | `stdio` |
```bash macOS/Linux
-export PROWLER_APP_API_KEY="pk_your_api_key_here"
+export PROWLER_API_KEY="pk_your_api_key_here"
export API_BASE_URL="https://api.prowler.com/api/v1"
export PROWLER_MCP_TRANSPORT_MODE="http"
```
```bash Windows PowerShell
-$env:PROWLER_APP_API_KEY="pk_your_api_key_here"
+$env:PROWLER_API_KEY="pk_your_api_key_here"
$env:API_BASE_URL="https://api.prowler.com/api/v1"
$env:PROWLER_MCP_TRANSPORT_MODE="http"
```
@@ -247,7 +247,7 @@ Never commit your API key to version control. Use environment variables or secur
For convenience, create a `.env` file in the `mcp_server` directory:
```bash .env
-PROWLER_APP_API_KEY=pk_your_api_key_here
+PROWLER_API_KEY=pk_your_api_key_here
API_BASE_URL=https://api.prowler.com/api/v1
PROWLER_MCP_TRANSPORT_MODE=stdio
```
diff --git a/docs/getting-started/products/index.mdx b/docs/getting-started/products/index.mdx
new file mode 100644
index 0000000000..d8def66737
--- /dev/null
+++ b/docs/getting-started/products/index.mdx
@@ -0,0 +1,52 @@
+---
+title: 'Prowler Product Families'
+description: 'Official names for Prowler Open Source projects and Prowler Products, including former product names.'
+boost: 2
+---
+
+Prowler ships two product families: Prowler Products, operated or licensed by the Prowler team, and Open Source projects, free to run and extend. This page is the reference for every official name. If a page or blog post uses a former name, the [Former Names](#former-names) table maps it to the current one.
+
+
+Read the [public announcement of the Prowler product families](https://prowler-workspace.slack.com/archives/C03JUQVM33L/p1784120677833319) in our Slack community.
+
+
+## Prowler Products
+
+| Name | Description |
+|------|-------------|
+| [Prowler Cloud](/getting-started/products/prowler-cloud) | Managed cloud security platform operated by the Prowler team. See [pricing](https://prowler.com/pricing). |
+| Prowler Private Cloud | Prowler Cloud deployed in your own environment. Formerly Prowler Enterprise. See [pricing](https://prowler.com/pricing). |
+| [Prowler Hub](https://hub.prowler.com) | Free public library of versioned checks, cloud service artifacts, and compliance frameworks. |
+| [Prowler Lighthouse AI](/getting-started/products/prowler-cloud-lighthouse) | AI security analyst capabilities within Prowler Cloud and Prowler Private Cloud. |
+| [Prowler MCP](/getting-started/products/prowler-mcp) | MCP server that connects AI assistants and agents to Prowler, including IDE plugins such as [Prowler for Claude Code](/user-guide/ai-agents/claude-code). |
+
+{/* Unreleased products. Uncomment these rows in the Prowler Products table when announced:
+| Prowler Registry | Distribution service for Prowler content such as checks and compliance frameworks. Free and paid tiers. |
+| Prowler Local Registry | Prowler Registry running in your own environment. Paid. |
+*/}
+
+
+Throughout this documentation, the green cloud icon in the sidebar marks sections and pages for capabilities that require a Prowler Cloud or Prowler Private Cloud [subscription](https://prowler.com/pricing).
+
+
+Products without a documentation page here are available through the Prowler team. [Contact us](https://prowler.com/contact) for details.
+
+## Open Source Projects
+
+| Name | Description |
+|------|-------------|
+| [Prowler CLI](/getting-started/products/prowler-cli) | Command line tool to run security scans across all supported providers. |
+| [Prowler Local Server](/getting-started/products/prowler-app) | Self-hosted web application and API to run scans, visualize findings, and manage cloud providers. Formerly Prowler App. |
+| [Prowler Local Dashboard](/user-guide/cli/tutorials/dashboard) | Local web dashboard to visualize scan results from Prowler CLI CSV outputs. Shipped with Prowler CLI. |
+| [Prowler SDK](/getting-started/products/prowler-sdk) | Python library that powers Prowler CLI and Prowler Local Server. Part of the [prowler repository](https://github.com/prowler-cloud/prowler). |
+
+## Former Names
+
+| Former name | Current name |
+|-------------|--------------|
+| Prowler App | [Prowler Local Server](/getting-started/products/prowler-app) |
+| Prowler Enterprise | Prowler Private Cloud |
+
+## Prowler for MSPs and MSSPs
+
+Prowler partners with managed service providers (MSPs) and managed security service providers (MSSPs) that operate Prowler for their customers. Visit [partners.prowler.com](https://partners.prowler.com) to become a partner.
diff --git a/docs/getting-started/products/prowler-app.mdx b/docs/getting-started/products/prowler-app.mdx
index f1f88decb7..5b50b16ebf 100644
--- a/docs/getting-started/products/prowler-app.mdx
+++ b/docs/getting-started/products/prowler-app.mdx
@@ -3,16 +3,16 @@ title: 'Prowler App overview'
description: 'Learn how the self-hosted Prowler App combines the Prowler UI, API, SDK, and MCP Server to configure scans, view findings, and manage cloud security posture.'
---
-Prowler App is a web application that simplifies running Prowler. It provides:
+Prowler Local Server is a self-hosted web application that simplifies running Prowler. It provides:
- **User-friendly interface** for configuring and executing scans
- Dashboard to **view results** and manage **security findings**
-
+
## Components
-Prowler App consists of four main components:
+Prowler Local Server consists of four main components:
- **Prowler UI**: User-friendly web interface for running Prowler and viewing results, powered by Next.js
- **Prowler API**: Backend API that executes Prowler scans and stores results, built with Django REST Framework
@@ -27,4 +27,42 @@ Supporting infrastructure includes:
- **Valkey**: In-memory database serving as message broker for Celery workers
- **Neo4j**: Graph database used by the Attack Paths feature to combine cloud inventory with Prowler findings (currently populated by AWS scans)
-
+```mermaid
+flowchart TB
+ user([User / Security Team])
+ cli([Prowler CLI])
+
+ subgraph APP["Prowler Local Server"]
+ ui["Prowler UI (Next.js)"]
+ api["Prowler API (Django REST Framework)"]
+ worker["API Worker (Celery)"]
+ beat["API Scheduler (Celery Beat)"]
+ mcp["Prowler MCP Server (Lighthouse AI tools)"]
+ end
+
+ sdk["Prowler SDK (Python)"]
+
+ subgraph DATA["Data Layer"]
+ pg[("PostgreSQL")]
+ valkey[("Valkey / Redis")]
+ neo4j[("Neo4j")]
+ end
+
+ providers["Providers"]
+
+ user --> ui
+ user --> cli
+ ui -->|REST| api
+ ui -->|MCP HTTP| mcp
+ mcp -->|REST| api
+ api --> pg
+ api --> valkey
+ beat -->|enqueue jobs| valkey
+ valkey -->|dispatch| worker
+ worker --> pg
+ worker -->|Attack Paths| neo4j
+ worker -->|invokes| sdk
+ cli --> sdk
+
+ sdk --> providers
+```
diff --git a/docs/getting-started/products/prowler-claude-code-plugin.mdx b/docs/getting-started/products/prowler-claude-code-plugin.mdx
deleted file mode 100644
index ccd89227db..0000000000
--- a/docs/getting-started/products/prowler-claude-code-plugin.mdx
+++ /dev/null
@@ -1,102 +0,0 @@
----
-title: 'Prowler for Claude Code plugin'
-description: 'Install the Prowler plugin for Claude Code to run cloud security and compliance assessments and remediate findings against Prowler Cloud connected accounts.'
----
-
-End-to-end cloud security and compliance from inside [Claude Code](https://www.claude.com/product/claude-code), powered by the [Prowler MCP server](/getting-started/products/prowler-mcp). The plugin lets Claude walk a Prowler Cloud-connected account through a compliance assessment and remediate findings until the chosen security or industry framework is compliant.
-
-
-**Preview**: this plugin is under active development. Please report issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join the [Slack community](https://goto.prowler.com/slack) for feedback.
-
-
-## Requirements
-
-
-
- Installed and signed in. See the [official install guide](https://www.claude.com/product/claude-code).
-
-
- The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
-
-
- Create one at [cloud.prowler.com/profile](https://cloud.prowler.com/profile).
-
-
-
-## Installation
-
-
-
- Inside a Claude Code session:
-
- ```text
- /plugin marketplace add prowler-cloud/prowler
- /plugin install prowler@prowler-plugins
- ```
-
-
- If you already have the repository checked out:
-
- ```text
- /plugin marketplace add /absolute/path/to/prowler
- /plugin install prowler@prowler-plugins
- ```
-
-
-
-## Configuration
-
-On first install, Claude Code prompts for your **Prowler API key**. The value is stored securely (macOS keychain or `~/.claude/.credentials.json`) and used to authenticate against Prowler Cloud.
-
-
-To rotate the key, uninstall and reinstall the plugin — Claude Code will prompt again.
-
-
-## Verify the installation
-
-In a Claude Code session:
-
-```text
-/mcp → "prowler" appears as a connected server
-/plugin → "prowler" enabled, skill listed as prowler:framework-compliance-triage
-```
-
-If `/mcp` reports the `prowler` server as failed, the most common cause is a rejected API key — re-issue one in Prowler Cloud and reinstall the plugin so it re-prompts.
-
-## Usage
-
-Open a conversation that mentions the framework you want to comply with. Examples:
-
-- *"Make my AWS production account compliant with CIS 4.0."*
-- *"Make my current Terraform project compliant with Prowler ThreatScore Compliance Framework based on the latest scan results."*
-- *"Help me get to 100% on PCI-DSS for this GCP project."*
-
-You pick a **primary tool** (Terraform, gh / az / aws CLI, web console, or mixed) and a **mode**:
-
-
-
- Claude shows each fix — target resource, exact commands, side effects, reversibility — and waits for your go-ahead before applying.
-
-
- Claude presents a single up-front plan grouped by shared fixes, waits for one confirmation, then proceeds. It pauses mid-loop if a fix has wide blast radius or a finding is not applicable.
-
-
-
-Claude tracks progress in a markdown report under `.prowler/` at your project root — one file per framework × account. Open it any time to see exactly where the flow is. When all findings are addressed, Claude proposes a fresh Prowler scan to verify everything end-to-end.
-
-## Uninstalling
-
-```text
-/plugin uninstall prowler@prowler-plugins
-/plugin marketplace remove prowler-plugins
-```
-
-The stored API key is removed automatically.
-
-## Troubleshooting
-
-| Symptom | Likely cause | Fix |
-| --- | --- | --- |
-| `/mcp` shows `prowler` as failed | Rejected API key | Generate a new one in Prowler Cloud and reinstall the plugin to re-prompt. |
-| Skill not invoked when expected | The skill description didn't match the prompt | Mention the framework name plus "compliance" or "compliant" in your prompt. |
-| "Framework not supported" | Prowler Hub does not list the framework for that provider | Open an issue or PR at [github.com/prowler-cloud/prowler](https://github.com/prowler-cloud/prowler). |
diff --git a/docs/getting-started/products/prowler-cli.mdx b/docs/getting-started/products/prowler-cli.mdx
index 1233d3ce75..d58d20136d 100644
--- a/docs/getting-started/products/prowler-cli.mdx
+++ b/docs/getting-started/products/prowler-cli.mdx
@@ -10,12 +10,12 @@ prowler
```

-## Prowler Dashboard
+## Prowler Local Dashboard
```console
prowler dashboard
```
-
+
Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including:
diff --git a/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx b/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
index 6250e6f3fb..d926c9ea9b 100644
--- a/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
+++ b/docs/getting-started/products/prowler-cloud-aws-marketplace.mdx
@@ -5,7 +5,7 @@ description: 'Subscribe to Prowler Cloud through the AWS Marketplace listing, se
This section contains the instructions to subscribe to **Prowler Cloud** through the **AWS Marketplace**.
-## How to subscribe
+## How to Subscribe
To get to the **Prowler Cloud** product listing in the AWS Marketplace, and click the `View purchase options` button:
@@ -17,15 +17,15 @@ To get to the **Prowler Cloud** product listing in the AWS Marketplace, and clic

-## Set up your account
+## Set Up Your Account
After you have subscribed to the **Prowler Cloud** product, you will need to set up your **Prowler Cloud** account:
-1. Click the `Set up your account` button:
+1. Click the `Set up your account` button:

-2. You will be redirected to **Prowler Cloud Sign In** page. You can sign in with an exsiting account or sign up with a new account.:
+2. You will be redirected to **Prowler Cloud Sign In** page. You can sign in with an existing account or sign up with a new account:

diff --git a/docs/getting-started/products/prowler-cloud-lighthouse.mdx b/docs/getting-started/products/prowler-cloud-lighthouse.mdx
index de0f98ff25..63fc4bb87c 100644
--- a/docs/getting-started/products/prowler-cloud-lighthouse.mdx
+++ b/docs/getting-started/products/prowler-cloud-lighthouse.mdx
@@ -4,6 +4,7 @@ description: 'Explore the enhanced Lighthouse AI on Prowler Cloud: persistent ch
---
import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
+import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler Cloud runs an enhanced version of Lighthouse AI in Open Source repository, the Agentic Cloud Defender that helps teams understand, prioritize, and remediate security findings across cloud environments.
@@ -25,6 +26,9 @@ The Agentic Cloud Defender does more than answer questions, it helps teams **fin
Switch between the standard interface and a chat-first agentic view.
+
+ Open Lighthouse AI as a side panel from any page to get help in context.
+
Credentials are validated automatically when a provider is configured.
@@ -32,12 +36,38 @@ The Agentic Cloud Defender does more than answer questions, it helps teams **fin
## Chat View
+
+
Lighthouse AI is no longer a separate section in the left navigation. Prowler Cloud now offers two application views: a normal view for browsing dashboards, findings, and configuration, and an agentic chat view, powered by Lighthouse AI, for conversational, multi-step security analysis. Conversations are saved automatically, so earlier sessions can be reopened and resumed at any time.
Promoting the chat to a top-level view gives Lighthouse AI the room it needs for a fully agentic workflow and makes the Agentic Cloud Defender a primary way to work in Prowler Cloud.
+### Side Panel
+
+
+
+You do not have to switch to the full chat view to reach Lighthouse AI. A side panel is available on every page of Prowler Cloud. While collapsed it stays out of the way; open it from any dashboard, findings list, or configuration screen to ask questions without leaving what you are working on. Open it using the Lighthouse AI button, circled in red in the image below.
+
+
+
+Once open, the panel slides in alongside your current page and shares the same agent, tools, and persistent chat sessions as the full Chat View, so a conversation started in the panel can be reopened and continued later from either place.
+
+
+
+- **Available everywhere:** Summon the assistant from any page while you keep working in the normal view.
+- **Context-aware help:** Ask about the findings, resources, or compliance data you are currently looking at.
+- **Continuous sessions:** Conversations opened in the side panel are saved alongside the rest of your chat history.
+
+### Context-Aware Chat
+
+
+
+The panel knows where you are in the app. Messages carry the page you are on and, when a finding or resource is open in the side panel, its metadata too, so questions like "explain this" resolve against what is on screen. The active context appears as a chip in the composer, circled in red in the image below, and each page offers contextual suggestions to start from.
+
+
+
### Tool Usage
Lighthouse AI on Prowler Cloud renders the agent's work as it happens, so responses are easier to follow and to trust. Tool calls and reasoning steps appear in the order they occur within the conversation.
@@ -64,6 +94,53 @@ At the top of the configuration page, the optional **Business Context** field le
Lighthouse AI on Prowler Cloud supports OpenAI, Amazon Bedrock, and OpenAI-compatible providers, with GPT-5.5 as the default. For per-provider setup and how to switch the default provider or model, see [Using Multiple LLM Providers](/user-guide/tutorials/prowler-cloud-lighthouse-multi-llm).
+## Capabilities
+
+Lighthouse AI works through the [Prowler MCP Server](/getting-started/products/prowler-mcp), which gives the agent a growing catalog of tools to explore and act on your security data. These actions run inside Prowler and never modify your cloud resources. Everything the agent can do maps to one of the following capability areas.
+
+### Findings and Finding Groups
+
+- Search and filter security findings across every connected provider by severity, status, region, service, check, date range, and muted state.
+- Retrieve full finding details, including remediation guidance, check metadata, and affected resources.
+- Summarize findings with aggregate statistics and trends.
+- Browse finding groups aggregated by check and drill down into the specific resources each group affects.
+
+### Resources
+
+- List and filter cloud resources by provider, region, service, resource type, and tags.
+- Inspect a resource's configuration, metadata, and related findings.
+- Review the timeline of cloud API actions performed on a resource (AWS CloudTrail), including who did what and when.
+- Get an aggregate overview of the resources Prowler has discovered.
+
+### Compliance
+
+- Review high-level compliance status across all frameworks, with pass/fail statistics per framework.
+- Get a requirement-level breakdown for a specific framework, including failed requirements and their associated findings.
+
+### Attack Paths
+
+- List Attack Paths scans and discover the queries available for each completed scan.
+- Run graph-based queries to reveal privilege-escalation chains and exploitable misconfigurations.
+- Retrieve the Cartography graph schema to build accurate custom queries.
+
+### Scans and Providers
+
+- List, inspect, and rename security scans across providers.
+- Trigger manual scans and schedule automated daily scans for continuous monitoring.
+- Search connected providers and check their connection status, connect new providers, or remove existing ones.
+
+### Muting
+
+- Manage the mutelist for pattern-based bulk muting.
+- Create, update, list, and delete finding-specific mute rules, each with a documented reason and audit trail.
+
+### Security Check Catalog and Documentation
+
+- Browse and search the Prowler Hub catalog of security checks and compliance frameworks, including check code and automated fixers.
+- Search and retrieve official Prowler documentation to answer how-to and product questions.
+
+For the complete list of underlying tools, see the [Prowler MCP Tools Reference](/getting-started/basic-usage/prowler-mcp-tools).
+
## FAQ
**Which LLM providers are supported?**
@@ -72,14 +149,18 @@ OpenAI (GPT models, including the default GPT-5.5), Amazon Bedrock (Claude, Llam
**Can Lighthouse AI change my cloud environment?**
-No. Lighthouse AI has read-only access to security data and no tools to modify resources, even when the connected cloud credentials would allow changes.
+No. Lighthouse AI cannot modify the resources in your connected cloud providers (AWS, Azure, GCP, and others). It has read-only access to that environment and no tools to change it, even when the connected cloud credentials would allow it.
+
+**Can Lighthouse AI change my Prowler Cloud environment?**
+
+Yes. Lighthouse AI can take action within Prowler Cloud itself, such as connecting or removing providers, triggering and scheduling scans, and managing mute rules and the mutelist. See [Capabilities](#capabilities) for the full list of what it can do. These actions only affect your Prowler Cloud workspace, never the resources in your cloud providers.
## Looking for the Open Source Version?
-Lighthouse AI is also available in the self-hosted, open-source Prowler App. For its capabilities, FAQs, and limitations, see the open-source documentation.
+Lighthouse AI is also available in the open-source Prowler Local Server. For its capabilities, FAQs, and limitations, see the open-source documentation.
- Capabilities, FAQs, and limitations for Lighthouse AI in the open-source Prowler App
+ Capabilities, FAQs, and limitations for Lighthouse AI in the open-source Prowler Local Server
## Getting Help
diff --git a/docs/getting-started/products/prowler-cloud.mdx b/docs/getting-started/products/prowler-cloud.mdx
index e609df6ac7..cf96cacebc 100644
--- a/docs/getting-started/products/prowler-cloud.mdx
+++ b/docs/getting-started/products/prowler-cloud.mdx
@@ -5,7 +5,7 @@ description: 'Prowler Cloud is the managed SaaS on Prowler open source, with con
[Prowler Cloud](https://prowler.com) makes Cloud Security easy and enables your team to build trust in their deployed services and applications.
-Prowler Cloud Automates scanning single or multiple accounts and has all of the benefits of Prowler Open Source, plus hands-off continuous monitoring, auto-scaling workers for faster execution, integrations, personalized support options and out of the box social authentication.
+Prowler Cloud automates scanning single or multiple accounts and has all of the benefits of Prowler Open Source, plus hands-off continuous monitoring, auto-scaling workers for faster execution, integrations, personalized support options and out of the box social authentication.

diff --git a/docs/getting-started/products/prowler-for-msps.mdx b/docs/getting-started/products/prowler-for-msps.mdx
new file mode 100644
index 0000000000..2b84ecf40d
--- /dev/null
+++ b/docs/getting-started/products/prowler-for-msps.mdx
@@ -0,0 +1,78 @@
+---
+title: "Prowler for MSPs and MSSPs"
+sidebarTitle: "Overview"
+---
+
+Prowler for MSPs and MSSPs is a dedicated console for Managed Service Providers (MSPs), Managed Security Service Providers (MSSPs), resellers and consultants who run cloud security for other organizations. It lets a provider onboard customers, group them, manage a team, and operate each customer's Prowler Cloud tenant on their behalf.
+
+The console is available at [partners.prowler.com](https://partners.prowler.com).
+
+
+
+## What You Get
+
+* **Customer onboarding:** provision a Prowler Cloud tenant for each customer, with a billing plan selected up front.
+* **Delegated access:** open any customer's Prowler Cloud tenant from the console. Every action is attributed to you acting on behalf of that customer.
+* **Team and roles:** invite team members by email and assign a role that governs what they can do.
+* **Consolidated billing:** each customer carries its own plan, with month-to-date revenue reported across every customer.
+* **Branding:** upload your logo and preview its intended placement in Settings.
+
+## Core Concepts
+
+Three objects make up the model. Getting these straight makes the rest of the documentation easy to follow.
+
+| Object | What it is |
+|---|---|
+| **Partner organization** | The provider's own company. The top-level container for everything below, created at sign-up. |
+| **Customer** | One of the provider's customers. Each customer maps to a Prowler Cloud tenant and carries its own billing plan. |
+| **Team member** | A user in the partner organization, holding a role that governs what they can do. |
+
+## How It Relates to Prowler Cloud
+
+| | Prowler Cloud | Prowler for MSPs and MSSPs |
+|---|---|---|
+| **Audience** | End customers | MSPs, MSSPs, resellers, consultants |
+| **Console** | [cloud.prowler.com](https://cloud.prowler.com) | [partners.prowler.com](https://partners.prowler.com) |
+| **Scope** | One organization's own cloud accounts | Many customer organizations |
+| **Billing** | Each organization pays for itself | The provider manages a plan per customer |
+| **Branding** | Prowler-branded | Logo upload and placement preview in Settings |
+
+Your customers keep signing in to Prowler Cloud with their own users. Provider-side access is **additive** — it does not replace or restrict customer-side users.
+
+## The Console at a Glance
+
+Signing in lands you on the **Dashboard**. The sidebar carries:
+
+| Entry | What it does | Visible to |
+|---|---|---|
+| **Dashboard** | Partner Insights, a Billing Overview card and an Active Customers table | Everyone |
+| **Customers** | Review customer posture and billing and open customer tenants; Superadmins can also add customers | Everyone |
+| **Team** | Invite, re-invite, disable and remove team members | Roles with **Manage members** |
+| **Settings** | Profile, Partner Code, branding and security | Everyone; editing requires **Manage settings** |
+
+
+
+**Partner Insights** is the top row: **Total Customers**, broken down into active and non-paid; **Cloud Accounts**, broken down by cloud provider; and **Monitored Resources**, with a note on organizations whose critical risk has grown. Each card carries a 30-day trend.
+
+Below it, **Billing Overview** reports monthly expenses against the previous month and splits revenue for the period into annual, monthly and overage. **Active Customers** lists your customers with their provider count, resource count and last completed scan and, for Superadmins, carries its own **Add Customer** button.
+
+## Getting Access
+
+Sign-up is self-service, approval is not. Register at [partners.prowler.com/sign-up](https://partners.prowler.com/sign-up), then verify your email address — the organization sits in **Pending email verification** until you do, and the Prowler team does not review it before that. Verifying moves the organization to **Pending approval**. Once approved, you can invite your team and start onboarding customers.
+
+## Next Steps
+
+
+
+ Register, verify your email, and get approved.
+
+
+ Lifecycle, settings, Partner Code and closing your organization.
+
+
+ Add customers and open their Prowler Cloud tenants.
+
+
+ Invite team members and assign roles.
+
+
diff --git a/docs/getting-started/products/prowler-lighthouse-ai.mdx b/docs/getting-started/products/prowler-lighthouse-ai.mdx
index 348894da8d..3005830312 100644
--- a/docs/getting-started/products/prowler-lighthouse-ai.mdx
+++ b/docs/getting-started/products/prowler-lighthouse-ai.mdx
@@ -97,7 +97,7 @@ Lighthouse AI supports three providers:
For detailed configuration instructions, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
-**2. Why some models don't appear in Lighthouse AI?**
+**2. Why don't some models appear in Lighthouse AI?**
LLM providers offer different types of models. Not every model can be integrated with Lighthouse AI (for example, text-to-speech, vision, embedding, computer use, etc.).
diff --git a/docs/getting-started/products/prowler-mcp.mdx b/docs/getting-started/products/prowler-mcp.mdx
index 4addaf3af4..54ea8e1530 100644
--- a/docs/getting-started/products/prowler-mcp.mdx
+++ b/docs/getting-started/products/prowler-mcp.mdx
@@ -9,34 +9,70 @@ description: 'The Prowler MCP Server exposes Prowler Cloud, Prowler App, Prowler
**Preview Feature**: This MCP server is currently under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
+## Quickest Way to Connect: Cloud MCP Server
+
+The fastest way to get started is the **Cloud MCP Server** at `https://mcp.prowler.com/mcp` — no installation, always up to date, and maintained by Prowler. Just point your MCP client at the URL and authenticate with a [Prowler API key](/user-guide/tutorials/prowler-app-api-keys) as a Bearer token:
+
+```json
+{
+ "mcpServers": {
+ "prowler": {
+ "url": "https://mcp.prowler.com/mcp",
+ "headers": {
+ "Authorization": "Bearer "
+ }
+ }
+ }
+}
+```
+
+
+ Step-by-step setup for Claude Code, Codex, Cursor, VS Code, and other agents.
+
+
+
+Prefer to run it yourself? The **Local MCP Server** runs on your own machine or infrastructure. The Cloud MCP Server additionally provides the `prowler_cloud_*` tools for Prowler Cloud-specific features: [Alerts](/user-guide/tutorials/prowler-alerts), [Findings Triage](/user-guide/tutorials/prowler-app-findings-triage), [Scan Scheduling](/user-guide/tutorials/prowler-scan-scheduling), and Scan Configurations. See [Cloud vs Local MCP Server](#cloud-vs-local-mcp-server).
+
+
## What is the Model Context Protocol?
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard developed by Anthropic that enables AI assistants to securely connect to external data sources and tools. It functions as a universal adapter enabling AI assistants to interact with various services through a standardized interface.
## Key Capabilities
-The Prowler MCP Server provides three main integration points:
+The Prowler MCP Server provides four integration points:
-### 1. Prowler Cloud and Prowler App (Self-Managed)
+### 1. Prowler Cloud, Private Cloud & Local Server
-Full access to Prowler Cloud platform and self-managed Prowler App for:
+Full access to your Prowler deployment — Prowler Cloud, Prowler Private Cloud, or Prowler Local Server — for:
- **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments
- **Provider Management**: Create, configure, and manage your configured Prowler providers (AWS, Azure, GCP, etc.)
-- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments
+- **Scan Orchestration**: Trigger on-demand scans, track their progress, and schedule a daily scan
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting lists/rules to suppress non-relevant findings
- **Attack Paths Analysis**: Analyze privilege escalation chains and security misconfigurations through graph-based analysis of cloud resource relationships
+- **Integrations Management**: Set up and troubleshoot where Prowler sends its results (Amazon S3, AWS Security Hub, Jira), and turn findings into Jira work items
+- **User & Role Management**: List the users in your tenant, identify the authenticated user, browse RBAC roles, and set the role a user holds
-### 2. Prowler Hub
+### 2. Prowler Cloud Management
+
+Prowler Cloud-only tools for configuration and workflows that a Prowler Local Server does not provide. These are exposed only by the [Cloud MCP Server](#cloud-vs-local-mcp-server):
+
+- **Scan Configurations**: Create reusable check and compliance selections and attach them to providers.
+- **Findings Triage**: Record review statuses and notes for individual findings, without suppressing them.
+- **Scan Scheduling**: Configure daily, interval, weekly, or monthly recurring scans, one provider at a time or in bulk.
+- **Alerts**: Build and dry-run alert rule conditions, manage email recipients, and review fired alerts.
+
+### 3. Prowler Hub
Access to Prowler's comprehensive security knowledge base:
-- **Security Checks Catalog**: Browse and search **over 1000 security checks** across multiple cloud providers.
+- **Security Checks Catalog**: Browse and search **over 2,000 security checks** across multiple cloud providers.
- **Check Implementation**: View the Python code that powers each security check.
- **Automated Fixers**: Access remediation scripts for common security issues.
- **Compliance Frameworks**: Explore mappings to **over 70 compliance standards and frameworks**.
- **Provider Services**: View available services and checks for each cloud provider.
-### 3. Prowler Documentation
+### 4. Prowler Documentation
Search and retrieve official Prowler documentation:
- **Intelligent Search**: Full-text search across all Prowler documentation.
@@ -45,12 +81,57 @@ Search and retrieve official Prowler documentation:
## MCP Server Architecture
-The following diagram illustrates the Prowler MCP Server architecture and its integration points:
+The following diagram illustrates the Prowler MCP Server architecture and its integration points. MCP clients connect to either the **Cloud MCP Server** (recommended) or a **Local MCP Server**. Both reach the same Prowler backends and share the `prowler_*`, `prowler_hub_*`, and `prowler_docs_*` tools; the Cloud MCP Server additionally exposes the Cloud-only `prowler_cloud_*` tools:
-
+```mermaid
+flowchart LR
+ subgraph HOSTS["MCP Clients"]
+ chat["Chat Interfaces (Claude Desktop, LobeChat)"]
+ ide["IDEs and Code Editors (Claude Code, Cursor)"]
+ apps["Other AI Applications (5ire, custom agents)"]
+ end
-The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components:
-- Prowler Cloud/App for security operations
+ subgraph SERVERS["Prowler MCP Server"]
+ direction TB
+ cloud["Cloud MCP Server (Recommended) mcp.prowler.com/mcp · HTTP Managed by Prowler · always up to date Adds the Cloud-only prowler_cloud_* tools"]
+ local["Local MCP Server Self-run · STDIO or HTTP Python 3.12+ or Docker You manage updates"]
+ end
+
+ subgraph TOOLS["Prowler MCP Tools"]
+ prowler_tools["prowler_* tools (API key or JWT auth) Findings · Finding Groups · Providers Scans · Resources · Muting · Compliance Attack Paths · Integrations · Users · Roles"]
+ cloud_tools["prowler_cloud_* tools (API key or JWT auth · Cloud only) Alerts · Findings Triage Scan Scheduling · Scan Configurations"]
+ hub_tools["prowler_hub_* tools (no auth) Checks Catalog · Check Code Fixers · Compliance Frameworks"]
+ docs_tools["prowler_docs_* tools (no auth) Search · Document Retrieval"]
+ end
+
+ api["Prowler API (REST) Cloud · Private Cloud · Local Server"]
+ hub["hub.prowler.com (REST)"]
+ docs["docs.prowler.com (Mintlify)"]
+
+ chat -->|HTTP| cloud
+ ide -->|HTTP| cloud
+ apps -->|HTTP| cloud
+ chat -->|STDIO or HTTP| local
+ ide -->|STDIO or HTTP| local
+ apps -->|STDIO or HTTP| local
+
+ cloud --> prowler_tools
+ cloud --> cloud_tools
+ cloud --> hub_tools
+ cloud --> docs_tools
+ local --> prowler_tools
+ local --> hub_tools
+ local --> docs_tools
+
+ prowler_tools -->|REST| api
+ cloud_tools -->|REST| api
+ hub_tools -->|REST| hub
+ docs_tools -->|REST| docs
+```
+
+The architecture shows how AI assistants connect through the MCP protocol to access Prowler's four namespaced components:
+- Prowler Cloud, Prowler Private Cloud, or Prowler Local Server for security operations
+- Prowler Cloud management for Cloud-only configuration and workflows
- Prowler Hub for security knowledge
- Prowler Documentation for guidance and reference.
@@ -61,8 +142,15 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
**Security Operations**
- "Show me all critical findings from my AWS production accounts"
- "Register my new AWS account in Prowler and run a scheduled scan every day"
-- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
+- "List all muted findings and flag the ones whose mute reason is too weak for their severity"
- "Run an attack paths query to find EC2 instances exposed to the Internet with access to sensitive S3 buckets"
+- "Send my failed CIS findings for this provider to Jira as work items"
+
+**Prowler Cloud Management** (Cloud MCP Server only)
+- "Preview an alert rule for critical AWS findings and create it for my confirmed recipients"
+- "Show the triage notes for this finding and mark it as under review"
+- "Apply a weekly Monday 06:00 scan schedule to every AWS provider"
+- "Create a scan configuration that runs only CIS checks and attach it to my production providers"
**Security Research**
- "Explain what the S3 bucket public access Prowler check does"
@@ -89,8 +177,8 @@ REQUIREMENTS:
DATA TO FETCH:
Use these MCP tools in this order:
-1. Prowler app list providers - To get all available configured provider in the account
-2. Prowler app get latest findings - To get findings information, if there are so many you can use the filter_fields to get less information, or pagination to get in different batches
+1. Prowler list providers - To get all available configured provider in the account
+2. Prowler get latest findings - To get findings information, if there are so many you can use the filter_fields to get less information, or pagination to get in different batches
3. For most critical findings you can get more context and remediation with Prowler Hub to get remediations for example
DESIGN REQUIREMENTS:
@@ -127,65 +215,48 @@ Generate the complete HTML file and display it
>
-## Deployment Options
+## Cloud vs Local MCP Server
-Prowler MCP Server can be used in three ways:
+There are two ways to run the Prowler MCP Server. For almost everyone, the **Cloud MCP Server** is the right choice — it needs no installation and is maintained by Prowler. The **Local MCP Server** exists for users who need to run it on their own machine or infrastructure.
-### 1. Prowler Cloud MCP Server
+| | ☁️ **Cloud MCP Server** (Recommended) | 💻 **Local MCP Server** |
+|---|---|---|
+| **Cloud-only tools** (`prowler_cloud_*`) | ✅ Alerts, Findings Triage, Scan Scheduling, Scan Configurations | ❌ Not available |
+| **Endpoint** | `https://mcp.prowler.com/mcp` | Runs on your machine or infrastructure |
+| **Setup** | Just configure your MCP client | Install via Docker, or source |
+| **Transport** | HTTP | STDIO (subprocess) or self-hosted HTTP |
+| **Maintenance** | Managed by Prowler, always up to date | You manage updates |
+| **Requirements** | None (just an MCP client) | Python 3.12+ or Docker |
+| **Authentication** | API key or JWT token | API key/JWT (HTTP) or env vars (STDIO) |
-**Use Prowler's managed MCP server at `https://mcp.prowler.com/mcp`**
+### ☁️ Cloud MCP Server (Recommended)
-- No installation required.
-- Managed and maintained by Prowler team.
-- Authentication to Prowler Cloud or Prowler App (self-managed) via API key or JWT token.
+Prowler's managed MCP server at `https://mcp.prowler.com/mcp`. No installation, always up to date, and it includes the `prowler_cloud_*` tools for Prowler Cloud-specific features: Alerts, Findings Triage, Scan Scheduling, and Scan Configurations. This is the path we recommend for nearly all users — go straight to the [Configuration guide](/getting-started/basic-usage/prowler-mcp#cloud-mcp-server-configuration-recommended).
-### 2. Local STDIO Mode
+### 💻 Local MCP Server
-**Run the server locally on your machine**
+Run the server yourself when you need full control over the deployment. It connects to Prowler Cloud, Prowler Private Cloud, or Prowler Local Server and can run in two modes:
-- Runs as a subprocess of your MCP client.
-- Possibility to connect to a self-hosted Prowler App (e.g. self-hosted Prowler App).
-- Authentication to Prowler Cloud or Prowler App (self-managed) via environment variables.
-- Requires Python 3.12+ or Docker.
+- **STDIO mode** — the server runs as a subprocess of your MCP client. Authentication via environment variables.
+- **Self-hosted HTTP mode** — deploy your own remote HTTP server. Authentication via API key or JWT token.
-### 3. Self-Hosted HTTP Mode
-
-**Deploy your own remote MCP server**
-
-- Full control over deployment.
-- Possibility to connect to a self-hosted Prowler App (e.g. self-hosted Prowler App).
-- Authentication to Prowler App (self-managed) via API key or JWT token.
-- Requires Python 3.12+ or Docker.
-
-## Requirements
-
-Requirements vary based on deployment option:
-
-**For Prowler Cloud MCP Server:**
-- Prowler Cloud account and API key (only for Prowler Cloud/App features)
-
-**For self-hosted STDIO/HTTP Mode:**
-- Python 3.12+ or Docker
-- Network access to:
- - `https://hub.prowler.com` (for Prowler Hub)
- - `https://docs.prowler.com` (for Prowler Documentation)
- - Prowler Cloud API or self-hosted Prowler App API (for Prowler Cloud/App features)
+Both require Python 3.12+ or Docker, plus network access to `https://hub.prowler.com` (Prowler Hub), `https://docs.prowler.com` (Prowler Documentation), and the Prowler API or Prowler Local Server API (Prowler features). See the [Installation guide](/getting-started/installation/prowler-mcp) to get started.
-**No Authentication Required**: Prowler Hub and Prowler Documentation features work without authentication in both deployment options. A Prowler API key is only required to access Prowler Cloud or Prowler App (Self-Managed) features.
+**No Authentication Required**: Prowler Hub and Prowler Documentation features work without authentication on both the Cloud and Local MCP Server. A Prowler API key is only required to access Prowler features (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server).
## Next Steps
-
- Install the Prowler MCP Server using uv or Docker
-
- Configure your MCP client to connect to the server
+ Connect your MCP client to the Cloud MCP Server
+
+
+ Explore all available tools and capabilities
-
- Explore all available tools and capabilities
+
+ Run the Local MCP Server yourself using Docker, source, or uvx
diff --git a/docs/getting-started/products/prowler-sdk.mdx b/docs/getting-started/products/prowler-sdk.mdx
new file mode 100644
index 0000000000..b19f576e12
--- /dev/null
+++ b/docs/getting-started/products/prowler-sdk.mdx
@@ -0,0 +1,11 @@
+---
+title: 'Prowler SDK'
+---
+
+Prowler SDK is the Python library that powers Prowler CLI and Prowler Local Server. It implements the providers, services, and security checks that every Prowler product runs.
+
+To use or extend Prowler SDK, start with the Developer Guide:
+
+
+ Providers, services, checks, and testing: everything needed to work with Prowler SDK.
+
diff --git a/docs/images/add-provider.png b/docs/images/add-provider.png
index 4e986e3f1a..f0e78930e8 100644
Binary files a/docs/images/add-provider.png and b/docs/images/add-provider.png differ
diff --git a/docs/images/changelog/v5.31.0-dora-alibaba.png b/docs/images/changelog/v5.31.0-dora-alibaba.png
new file mode 100644
index 0000000000..04d6f7cf94
Binary files /dev/null and b/docs/images/changelog/v5.31.0-dora-alibaba.png differ
diff --git a/docs/images/changelog/v5.31.0-onboarding-1.png b/docs/images/changelog/v5.31.0-onboarding-1.png
new file mode 100644
index 0000000000..3881c7a6d1
Binary files /dev/null and b/docs/images/changelog/v5.31.0-onboarding-1.png differ
diff --git a/docs/images/changelog/v5.31.0-onboarding-2.png b/docs/images/changelog/v5.31.0-onboarding-2.png
new file mode 100644
index 0000000000..3cd9ad5e2d
Binary files /dev/null and b/docs/images/changelog/v5.31.0-onboarding-2.png differ
diff --git a/docs/images/changelog/v5.31.0-schedule-1.png b/docs/images/changelog/v5.31.0-schedule-1.png
new file mode 100644
index 0000000000..4de4838714
Binary files /dev/null and b/docs/images/changelog/v5.31.0-schedule-1.png differ
diff --git a/docs/images/changelog/v5.31.0-schedule-2.png b/docs/images/changelog/v5.31.0-schedule-2.png
new file mode 100644
index 0000000000..6897a53c61
Binary files /dev/null and b/docs/images/changelog/v5.31.0-schedule-2.png differ
diff --git a/docs/images/changelog/v5.31.0-schedule-3.png b/docs/images/changelog/v5.31.0-schedule-3.png
new file mode 100644
index 0000000000..db7319b607
Binary files /dev/null and b/docs/images/changelog/v5.31.0-schedule-3.png differ
diff --git a/docs/images/changelog/v5.32.0-config-1.png b/docs/images/changelog/v5.32.0-config-1.png
new file mode 100644
index 0000000000..561eaa865d
Binary files /dev/null and b/docs/images/changelog/v5.32.0-config-1.png differ
diff --git a/docs/images/changelog/v5.32.0-config-2.png b/docs/images/changelog/v5.32.0-config-2.png
new file mode 100644
index 0000000000..2e57803e21
Binary files /dev/null and b/docs/images/changelog/v5.32.0-config-2.png differ
diff --git a/docs/images/changelog/v5.32.0-per-requirement-validation.png b/docs/images/changelog/v5.32.0-per-requirement-validation.png
new file mode 100644
index 0000000000..821e0cff8d
Binary files /dev/null and b/docs/images/changelog/v5.32.0-per-requirement-validation.png differ
diff --git a/docs/images/changelog/v5.32.0-provider-group-filter.png b/docs/images/changelog/v5.32.0-provider-group-filter.png
new file mode 100644
index 0000000000..39458a1a04
Binary files /dev/null and b/docs/images/changelog/v5.32.0-provider-group-filter.png differ
diff --git a/docs/images/changelog/v5.32.0-triage-1.png b/docs/images/changelog/v5.32.0-triage-1.png
new file mode 100644
index 0000000000..ce0ea3f9f1
Binary files /dev/null and b/docs/images/changelog/v5.32.0-triage-1.png differ
diff --git a/docs/images/changelog/v5.32.0-triage-2.png b/docs/images/changelog/v5.32.0-triage-2.png
new file mode 100644
index 0000000000..3c7abe387f
Binary files /dev/null and b/docs/images/changelog/v5.32.0-triage-2.png differ
diff --git a/docs/images/changelog/v5.33.0-lighthouse-ai-1.webp b/docs/images/changelog/v5.33.0-lighthouse-ai-1.webp
new file mode 100644
index 0000000000..2771d57edb
Binary files /dev/null and b/docs/images/changelog/v5.33.0-lighthouse-ai-1.webp differ
diff --git a/docs/images/changelog/v5.33.0-lighthouse-ai-2.webp b/docs/images/changelog/v5.33.0-lighthouse-ai-2.webp
new file mode 100644
index 0000000000..746d71f110
Binary files /dev/null and b/docs/images/changelog/v5.33.0-lighthouse-ai-2.webp differ
diff --git a/docs/images/changelog/v5.33.0-lighthouse-ai-3.webp b/docs/images/changelog/v5.33.0-lighthouse-ai-3.webp
new file mode 100644
index 0000000000..4bff7b1460
Binary files /dev/null and b/docs/images/changelog/v5.33.0-lighthouse-ai-3.webp differ
diff --git a/docs/images/changelog/v5.34.0-cross-provider-compliance-detail.png b/docs/images/changelog/v5.34.0-cross-provider-compliance-detail.png
new file mode 100644
index 0000000000..aa858ed64c
Binary files /dev/null and b/docs/images/changelog/v5.34.0-cross-provider-compliance-detail.png differ
diff --git a/docs/images/changelog/v5.34.0-cross-provider-compliance-overview.png b/docs/images/changelog/v5.34.0-cross-provider-compliance-overview.png
new file mode 100644
index 0000000000..70d27a51d0
Binary files /dev/null and b/docs/images/changelog/v5.34.0-cross-provider-compliance-overview.png differ
diff --git a/docs/images/changelog/v5.35.0-aws-orgs-wizard.png b/docs/images/changelog/v5.35.0-aws-orgs-wizard.png
new file mode 100644
index 0000000000..36fdd92db7
Binary files /dev/null and b/docs/images/changelog/v5.35.0-aws-orgs-wizard.png differ
diff --git a/docs/images/changelog/v5.35.0-lighthouse-ai-side-chat.png b/docs/images/changelog/v5.35.0-lighthouse-ai-side-chat.png
new file mode 100644
index 0000000000..829dffe4e9
Binary files /dev/null and b/docs/images/changelog/v5.35.0-lighthouse-ai-side-chat.png differ
diff --git a/docs/images/changelog/v5.35.0-new-menu.png b/docs/images/changelog/v5.35.0-new-menu.png
new file mode 100644
index 0000000000..f58a34e42e
Binary files /dev/null and b/docs/images/changelog/v5.35.0-new-menu.png differ
diff --git a/docs/images/changelog/v5.36.0-attack-paths-queries.png b/docs/images/changelog/v5.36.0-attack-paths-queries.png
new file mode 100644
index 0000000000..a6ede139f2
Binary files /dev/null and b/docs/images/changelog/v5.36.0-attack-paths-queries.png differ
diff --git a/docs/images/changelog/v5.36.0-finding-groups-jira.png b/docs/images/changelog/v5.36.0-finding-groups-jira.png
new file mode 100644
index 0000000000..3054b787ba
Binary files /dev/null and b/docs/images/changelog/v5.36.0-finding-groups-jira.png differ
diff --git a/docs/images/changelog/v5.38.0-user-sign-in-methods.png b/docs/images/changelog/v5.38.0-user-sign-in-methods.png
new file mode 100644
index 0000000000..f03682b14a
Binary files /dev/null and b/docs/images/changelog/v5.38.0-user-sign-in-methods.png differ
diff --git a/docs/images/cli/api-keys/create.png b/docs/images/cli/api-keys/create.png
index 54218c88f8..5dff1c6b7b 100644
Binary files a/docs/images/cli/api-keys/create.png and b/docs/images/cli/api-keys/create.png differ
diff --git a/docs/images/cli/api-keys/created.png b/docs/images/cli/api-keys/created.png
index a64b6faa74..f6fd9448f9 100644
Binary files a/docs/images/cli/api-keys/created.png and b/docs/images/cli/api-keys/created.png differ
diff --git a/docs/images/cli/api-keys/list.png b/docs/images/cli/api-keys/list.png
index 3c6020acfc..cc6aaf599b 100644
Binary files a/docs/images/cli/api-keys/list.png and b/docs/images/cli/api-keys/list.png differ
diff --git a/docs/images/cli/api-keys/management.png b/docs/images/cli/api-keys/management.png
index 85ebab8d02..bdd5b19d1d 100644
Binary files a/docs/images/cli/api-keys/management.png and b/docs/images/cli/api-keys/management.png differ
diff --git a/docs/images/cli/api-keys/update.png b/docs/images/cli/api-keys/update.png
index 81cc574801..b22ea7c14c 100644
Binary files a/docs/images/cli/api-keys/update.png and b/docs/images/cli/api-keys/update.png differ
diff --git a/docs/images/compliance.png b/docs/images/compliance.png
index b08fcc3651..f6df1abadc 100644
Binary files a/docs/images/compliance.png and b/docs/images/compliance.png differ
diff --git a/docs/images/compliance/prowler-app-across-providers-detail.png b/docs/images/compliance/prowler-app-across-providers-detail.png
new file mode 100644
index 0000000000..cd8891fb3e
Binary files /dev/null and b/docs/images/compliance/prowler-app-across-providers-detail.png differ
diff --git a/docs/images/compliance/prowler-app-across-providers-expanded.png b/docs/images/compliance/prowler-app-across-providers-expanded.png
new file mode 100644
index 0000000000..ce34664083
Binary files /dev/null and b/docs/images/compliance/prowler-app-across-providers-expanded.png differ
diff --git a/docs/images/compliance/prowler-app-across-providers-report.png b/docs/images/compliance/prowler-app-across-providers-report.png
new file mode 100644
index 0000000000..c4b0e55c56
Binary files /dev/null and b/docs/images/compliance/prowler-app-across-providers-report.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-card-download.png b/docs/images/compliance/prowler-app-compliance-card-download.png
index ec652f8774..00095d9edd 100644
Binary files a/docs/images/compliance/prowler-app-compliance-card-download.png and b/docs/images/compliance/prowler-app-compliance-card-download.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-detail-download.png b/docs/images/compliance/prowler-app-compliance-detail-download.png
index 96c4cfc980..9c4bd4ccf0 100644
Binary files a/docs/images/compliance/prowler-app-compliance-detail-download.png and b/docs/images/compliance/prowler-app-compliance-detail-download.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-detail-header.png b/docs/images/compliance/prowler-app-compliance-detail-header.png
index 03065cc7a5..e792cfb69f 100644
Binary files a/docs/images/compliance/prowler-app-compliance-detail-header.png and b/docs/images/compliance/prowler-app-compliance-detail-header.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-multiple-scans.png b/docs/images/compliance/prowler-app-compliance-multiple-scans.png
new file mode 100644
index 0000000000..60059781cf
Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-multiple-scans.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-overview.png b/docs/images/compliance/prowler-app-compliance-overview.png
index 03302d1902..f2aedb4303 100644
Binary files a/docs/images/compliance/prowler-app-compliance-overview.png and b/docs/images/compliance/prowler-app-compliance-overview.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-requirements-accordion.png b/docs/images/compliance/prowler-app-compliance-requirements-accordion.png
index 47ceba8f23..1530559f69 100644
Binary files a/docs/images/compliance/prowler-app-compliance-requirements-accordion.png and b/docs/images/compliance/prowler-app-compliance-requirements-accordion.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-single-scan-pins.png b/docs/images/compliance/prowler-app-compliance-single-scan-pins.png
new file mode 100644
index 0000000000..da63025167
Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-single-scan-pins.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-threatscore-card.png b/docs/images/compliance/prowler-app-compliance-threatscore-card.png
index 3bcf349c79..36e9b99744 100644
Binary files a/docs/images/compliance/prowler-app-compliance-threatscore-card.png and b/docs/images/compliance/prowler-app-compliance-threatscore-card.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-threatscore-detail.png b/docs/images/compliance/prowler-app-compliance-threatscore-detail.png
index 57e909a94c..f2ba5ce0b5 100644
Binary files a/docs/images/compliance/prowler-app-compliance-threatscore-detail.png and b/docs/images/compliance/prowler-app-compliance-threatscore-detail.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-watchlist-editor.png b/docs/images/compliance/prowler-app-compliance-watchlist-editor.png
new file mode 100644
index 0000000000..208735ff75
Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-watchlist-editor.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-watchlist-filtered.png b/docs/images/compliance/prowler-app-compliance-watchlist-filtered.png
new file mode 100644
index 0000000000..c64c169172
Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-watchlist-filtered.png differ
diff --git a/docs/images/compliance/prowler-app-compliance-watchlist-pins.png b/docs/images/compliance/prowler-app-compliance-watchlist-pins.png
new file mode 100644
index 0000000000..f8a3e3716e
Binary files /dev/null and b/docs/images/compliance/prowler-app-compliance-watchlist-pins.png differ
diff --git a/docs/images/compliance/prowler-app-cross-provider-detail.png b/docs/images/compliance/prowler-app-cross-provider-detail.png
new file mode 100644
index 0000000000..8ee82562e2
Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-detail.png differ
diff --git a/docs/images/compliance/prowler-app-cross-provider-report.png b/docs/images/compliance/prowler-app-cross-provider-report.png
new file mode 100644
index 0000000000..e3b45ef74e
Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-report.png differ
diff --git a/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png b/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png
new file mode 100644
index 0000000000..cd87e3ee35
Binary files /dev/null and b/docs/images/compliance/prowler-app-cross-provider-requirements-accordion.png differ
diff --git a/docs/images/compliance/prowler-app-overview-compliance-watchlist.png b/docs/images/compliance/prowler-app-overview-compliance-watchlist.png
new file mode 100644
index 0000000000..05e4cb5296
Binary files /dev/null and b/docs/images/compliance/prowler-app-overview-compliance-watchlist.png differ
diff --git a/docs/images/compliance_download.png b/docs/images/compliance_download.png
index 32aed141b3..15b4cbc1f1 100644
Binary files a/docs/images/compliance_download.png and b/docs/images/compliance_download.png differ
diff --git a/docs/images/compliance_section.png b/docs/images/compliance_section.png
index 2b64031550..f610d7d122 100644
Binary files a/docs/images/compliance_section.png and b/docs/images/compliance_section.png differ
diff --git a/docs/images/download_output.png b/docs/images/download_output.png
index 452851b84d..0608d23939 100644
Binary files a/docs/images/download_output.png and b/docs/images/download_output.png differ
diff --git a/docs/images/finding-groups-drawer.png b/docs/images/finding-groups-drawer.png
index 2c07f63249..088040360f 100644
Binary files a/docs/images/finding-groups-drawer.png and b/docs/images/finding-groups-drawer.png differ
diff --git a/docs/images/finding-groups-expanded.png b/docs/images/finding-groups-expanded.png
index 677df0020f..e19508bb19 100644
Binary files a/docs/images/finding-groups-expanded.png and b/docs/images/finding-groups-expanded.png differ
diff --git a/docs/images/finding-groups-list.png b/docs/images/finding-groups-list.png
index e70d2bb969..db98f7f9f8 100644
Binary files a/docs/images/finding-groups-list.png and b/docs/images/finding-groups-list.png differ
diff --git a/docs/images/finding-groups-other-findings.png b/docs/images/finding-groups-other-findings.png
index 35605a7710..6d7a2490f7 100644
Binary files a/docs/images/finding-groups-other-findings.png and b/docs/images/finding-groups-other-findings.png differ
diff --git a/docs/images/findings.png b/docs/images/findings.png
index e2ea9c56ce..db98f7f9f8 100644
Binary files a/docs/images/findings.png and b/docs/images/findings.png differ
diff --git a/docs/images/gcp-credentials.png b/docs/images/gcp-credentials.png
index 4f6dae3b1e..f8d69704d5 100644
Binary files a/docs/images/gcp-credentials.png and b/docs/images/gcp-credentials.png differ
diff --git a/docs/images/icons/cloud-bold.svg b/docs/images/icons/cloud-bold.svg
new file mode 100644
index 0000000000..623bb36c11
--- /dev/null
+++ b/docs/images/icons/cloud-bold.svg
@@ -0,0 +1 @@
+
diff --git a/docs/images/issues.png b/docs/images/issues.png
index 009bc0d447..483bf25d8d 100644
Binary files a/docs/images/issues.png and b/docs/images/issues.png differ
diff --git a/docs/images/kubernetes-credentials.png b/docs/images/kubernetes-credentials.png
index b461ed218a..14c282d005 100644
Binary files a/docs/images/kubernetes-credentials.png and b/docs/images/kubernetes-credentials.png differ
diff --git a/docs/images/lighthouse-architecture.mmd b/docs/images/lighthouse-architecture.mmd
index 47407544e9..6798801fb8 100644
--- a/docs/images/lighthouse-architecture.mmd
+++ b/docs/images/lighthouse-architecture.mmd
@@ -15,7 +15,7 @@ flowchart TB
llm["LLM Provider (OpenAI / Bedrock / OpenAI-compatible)"]
subgraph MCP["Prowler MCP Server"]
- app_tools["prowler_app_* tools (auth required)"]
+ app_tools["prowler_* tools (auth required)"]
hub_tools["prowler_hub_* tools (no auth)"]
docs_tools["prowler_docs_* tools (no auth)"]
end
@@ -29,7 +29,7 @@ flowchart TB
agent <-->|LLM API| llm
agent --> metatools
metatools --> mcpclient
- mcpclient -->|MCP HTTP · Bearer token for prowler_app_* only| app_tools
+ mcpclient -->|MCP HTTP · Bearer token for prowler_* only| app_tools
mcpclient -->|MCP HTTP| hub_tools
mcpclient -->|MCP HTTP| docs_tools
app_tools -->|REST| api
diff --git a/docs/images/log-in.png b/docs/images/log-in.png
index 9e6d410abe..134a074a1a 100644
Binary files a/docs/images/log-in.png and b/docs/images/log-in.png differ
diff --git a/docs/images/mutelist-ui-1.png b/docs/images/mutelist-ui-1.png
index 8114e64fdf..d274e0b729 100644
Binary files a/docs/images/mutelist-ui-1.png and b/docs/images/mutelist-ui-1.png differ
diff --git a/docs/images/mutelist-ui-2.png b/docs/images/mutelist-ui-2.png
index 847f7c1b16..ae6af02bd7 100644
Binary files a/docs/images/mutelist-ui-2.png and b/docs/images/mutelist-ui-2.png differ
diff --git a/docs/images/mutelist-ui-3.png b/docs/images/mutelist-ui-3.png
index da1951c533..814247207b 100644
Binary files a/docs/images/mutelist-ui-3.png and b/docs/images/mutelist-ui-3.png differ
diff --git a/docs/images/mutelist-ui-5.png b/docs/images/mutelist-ui-5.png
index 128bf30731..1eab7bf6bd 100644
Binary files a/docs/images/mutelist-ui-5.png and b/docs/images/mutelist-ui-5.png differ
diff --git a/docs/images/mutelist-ui-6.png b/docs/images/mutelist-ui-6.png
index 659eccb61e..733deaaca5 100644
Binary files a/docs/images/mutelist-ui-6.png and b/docs/images/mutelist-ui-6.png differ
diff --git a/docs/images/mutelist-ui-7.png b/docs/images/mutelist-ui-7.png
index e6352c97e9..fa1ff660e6 100644
Binary files a/docs/images/mutelist-ui-7.png and b/docs/images/mutelist-ui-7.png differ
diff --git a/docs/images/mutelist-ui-8.png b/docs/images/mutelist-ui-8.png
index 54e2110edb..0e81f3d024 100644
Binary files a/docs/images/mutelist-ui-8.png and b/docs/images/mutelist-ui-8.png differ
diff --git a/docs/images/mutelist-ui-9.png b/docs/images/mutelist-ui-9.png
index cba2ece1a3..52e99db624 100644
Binary files a/docs/images/mutelist-ui-9.png and b/docs/images/mutelist-ui-9.png differ
diff --git a/docs/images/organizations/authentication-details.png b/docs/images/organizations/authentication-details.png
index 2b4ae782cf..aec5060afd 100644
Binary files a/docs/images/organizations/authentication-details.png and b/docs/images/organizations/authentication-details.png differ
diff --git a/docs/images/organizations/cloud-providers-add.png b/docs/images/organizations/cloud-providers-add.png
index 21d0fadff3..f0e78930e8 100644
Binary files a/docs/images/organizations/cloud-providers-add.png and b/docs/images/organizations/cloud-providers-add.png differ
diff --git a/docs/images/organizations/delete-organization.png b/docs/images/organizations/delete-organization.png
new file mode 100644
index 0000000000..0c240746d1
Binary files /dev/null and b/docs/images/organizations/delete-organization.png differ
diff --git a/docs/images/organizations/discovery-timeout.png b/docs/images/organizations/discovery-timeout.png
new file mode 100644
index 0000000000..502827874b
Binary files /dev/null and b/docs/images/organizations/discovery-timeout.png differ
diff --git a/docs/images/organizations/gcp/gcp-authentication-details.png b/docs/images/organizations/gcp/gcp-authentication-details.png
new file mode 100644
index 0000000000..bd2800d146
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-authentication-details.png differ
diff --git a/docs/images/organizations/gcp/gcp-blocked-project.png b/docs/images/organizations/gcp/gcp-blocked-project.png
new file mode 100644
index 0000000000..d97b3da649
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-blocked-project.png differ
diff --git a/docs/images/organizations/gcp/gcp-console-org-id.png b/docs/images/organizations/gcp/gcp-console-org-id.png
new file mode 100644
index 0000000000..34b3b3ccc7
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-console-org-id.png differ
diff --git a/docs/images/organizations/gcp/gcp-delete-organization.png b/docs/images/organizations/gcp/gcp-delete-organization.png
new file mode 100644
index 0000000000..ab4fd9fba5
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-delete-organization.png differ
diff --git a/docs/images/organizations/gcp/gcp-discovery-timeout.png b/docs/images/organizations/gcp/gcp-discovery-timeout.png
new file mode 100644
index 0000000000..b21b5a5657
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-discovery-timeout.png differ
diff --git a/docs/images/organizations/gcp/gcp-gathering-projects.png b/docs/images/organizations/gcp/gcp-gathering-projects.png
new file mode 100644
index 0000000000..5fe3a213a9
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-gathering-projects.png differ
diff --git a/docs/images/organizations/gcp/gcp-inert-folder.png b/docs/images/organizations/gcp/gcp-inert-folder.png
new file mode 100644
index 0000000000..1bbff1d302
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-inert-folder.png differ
diff --git a/docs/images/organizations/gcp/gcp-launch-scan.png b/docs/images/organizations/gcp/gcp-launch-scan.png
new file mode 100644
index 0000000000..dbedeebc37
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-launch-scan.png differ
diff --git a/docs/images/organizations/gcp/gcp-organization-details-form.png b/docs/images/organizations/gcp/gcp-organization-details-form.png
new file mode 100644
index 0000000000..f4fa0f1dec
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-organization-details-form.png differ
diff --git a/docs/images/organizations/gcp/gcp-organization-row-actions.png b/docs/images/organizations/gcp/gcp-organization-row-actions.png
new file mode 100644
index 0000000000..4e2b9a485d
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-organization-row-actions.png differ
diff --git a/docs/images/organizations/gcp/gcp-providers-grouping.png b/docs/images/organizations/gcp/gcp-providers-grouping.png
new file mode 100644
index 0000000000..5e11d3e916
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-providers-grouping.png differ
diff --git a/docs/images/organizations/gcp/gcp-replace-credentials-apply.png b/docs/images/organizations/gcp/gcp-replace-credentials-apply.png
new file mode 100644
index 0000000000..f21b5b88cc
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-replace-credentials-apply.png differ
diff --git a/docs/images/organizations/gcp/gcp-replace-credentials-setup.png b/docs/images/organizations/gcp/gcp-replace-credentials-setup.png
new file mode 100644
index 0000000000..1ce257d3f0
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-replace-credentials-setup.png differ
diff --git a/docs/images/organizations/gcp/gcp-test-connections.png b/docs/images/organizations/gcp/gcp-test-connections.png
new file mode 100644
index 0000000000..830809247a
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-test-connections.png differ
diff --git a/docs/images/organizations/gcp/gcp-tree-view-projects.png b/docs/images/organizations/gcp/gcp-tree-view-projects.png
new file mode 100644
index 0000000000..5b163519b6
Binary files /dev/null and b/docs/images/organizations/gcp/gcp-tree-view-projects.png differ
diff --git a/docs/images/organizations/gcp/select-gcp-organizations-method.png b/docs/images/organizations/gcp/select-gcp-organizations-method.png
new file mode 100644
index 0000000000..62422d9ed5
Binary files /dev/null and b/docs/images/organizations/gcp/select-gcp-organizations-method.png differ
diff --git a/docs/images/organizations/gcp/select-gcp-provider.png b/docs/images/organizations/gcp/select-gcp-provider.png
new file mode 100644
index 0000000000..6a933fdc4a
Binary files /dev/null and b/docs/images/organizations/gcp/select-gcp-provider.png differ
diff --git a/docs/images/organizations/launch-scan.png b/docs/images/organizations/launch-scan.png
index 564c7674a1..ec6d3d3784 100644
Binary files a/docs/images/organizations/launch-scan.png and b/docs/images/organizations/launch-scan.png differ
diff --git a/docs/images/organizations/onboarding-flow.svg b/docs/images/organizations/onboarding-flow.svg
index f6e11fc0a3..b5ba7858a0 100644
--- a/docs/images/organizations/onboarding-flow.svg
+++ b/docs/images/organizations/onboarding-flow.svg
@@ -3,41 +3,37 @@
+
+
+ Onboarding Flow
-
+ 1
- Create Management
- Account Role
-
- Quick Create or Manual
- Allows Prowler to
- discover your org
- structure
+ Start the Wizard
+
+ In Prowler Cloud
+ Enter your Org ID
+ and OU/root target
-
-
-
-
- 2
- Deploy StackSet
-
- In AWS Console
- Creates ProwlerScan
- role in every
- member account
+ Deploy the Roles
+
+ Single CF Stack
+ Management role +
+ StackSet to members
+ in one CF stack
@@ -46,11 +42,11 @@
3
- Run the Wizard
-
- In Prowler Cloud
- Discovers accounts,
- tests connections
+ Discover & Connect
+
+ In Prowler Cloud
+ Discovers accounts,
+ tests connections
@@ -59,13 +55,13 @@
4
- Launch Scans
-
- Automatic
- Scans run on all
- connected accounts
- on your schedule
+ Launch Scans
+
+ Automatic
+ Scans run on all
+ connected accounts
+ on your schedule
- Steps 1 and 2 are done once in AWS | Steps 3 and 4 are done in Prowler Cloud
+ Step 2 runs once in AWS | Steps 1, 3 and 4 are in Prowler Cloud
diff --git a/docs/images/organizations/organization-details-form.png b/docs/images/organizations/organization-details-form.png
index 41b574f4c3..968098ad17 100644
Binary files a/docs/images/organizations/organization-details-form.png and b/docs/images/organizations/organization-details-form.png differ
diff --git a/docs/images/organizations/organization-row-actions.png b/docs/images/organizations/organization-row-actions.png
new file mode 100644
index 0000000000..9c8c87e846
Binary files /dev/null and b/docs/images/organizations/organization-row-actions.png differ
diff --git a/docs/images/organizations/replace-credentials-apply.png b/docs/images/organizations/replace-credentials-apply.png
new file mode 100644
index 0000000000..5bfa5fe45e
Binary files /dev/null and b/docs/images/organizations/replace-credentials-apply.png differ
diff --git a/docs/images/organizations/replace-credentials-setup.png b/docs/images/organizations/replace-credentials-setup.png
new file mode 100644
index 0000000000..822ca151ef
Binary files /dev/null and b/docs/images/organizations/replace-credentials-setup.png differ
diff --git a/docs/images/organizations/role-arn-field.png b/docs/images/organizations/role-arn-field.png
index 3f6832bfa4..a5e505b635 100644
Binary files a/docs/images/organizations/role-arn-field.png and b/docs/images/organizations/role-arn-field.png differ
diff --git a/docs/images/organizations/select-aws-provider.png b/docs/images/organizations/select-aws-provider.png
index 7b47b4b400..8702b1976d 100644
Binary files a/docs/images/organizations/select-aws-provider.png and b/docs/images/organizations/select-aws-provider.png differ
diff --git a/docs/images/organizations/select-organizations-method.png b/docs/images/organizations/select-organizations-method.png
index f4c4aa7c8f..67c38d74e4 100644
Binary files a/docs/images/organizations/select-organizations-method.png and b/docs/images/organizations/select-organizations-method.png differ
diff --git a/docs/images/organizations/two-roles-architecture.svg b/docs/images/organizations/two-roles-architecture.svg
index c67588b049..f8c40d5b21 100644
--- a/docs/images/organizations/two-roles-architecture.svg
+++ b/docs/images/organizations/two-roles-architecture.svg
@@ -47,7 +47,7 @@
- Deploy: Quick Create link or Manual
+ Deploy: single stack or standalone
@@ -86,7 +86,7 @@
- Deploy: via CloudFormation StackSet
+ Deploy: StackSet (single stack)Prowler discovers
diff --git a/docs/images/powerbi/download-compliance-scan.png b/docs/images/powerbi/download-compliance-scan.png
index 4626d90edd..00095d9edd 100644
Binary files a/docs/images/powerbi/download-compliance-scan.png and b/docs/images/powerbi/download-compliance-scan.png differ
diff --git a/docs/images/products/overview.png b/docs/images/products/overview.png
index 697e1f74dc..0a3d53ad75 100644
Binary files a/docs/images/products/overview.png and b/docs/images/products/overview.png differ
diff --git a/docs/images/products/prowler-app-architecture.mmd b/docs/images/products/prowler-app-architecture.mmd
index 0c13d580c3..da99d0ab96 100644
--- a/docs/images/products/prowler-app-architecture.mmd
+++ b/docs/images/products/prowler-app-architecture.mmd
@@ -1,8 +1,10 @@
+%% Source of truth for the architecture diagram.
+%% Inlined as native mermaid blocks in the root README.md and in docs/getting-started/products/prowler-app.mdx: keep all copies in sync.
flowchart TB
user([User / Security Team])
cli([Prowler CLI])
- subgraph APP["Prowler App"]
+ subgraph APP["Prowler Local Server"]
ui["Prowler UI (Next.js)"]
api["Prowler API (Django REST Framework)"]
worker["API Worker (Celery)"]
diff --git a/docs/images/provider-added.png b/docs/images/provider-added.png
index cfb94d2cab..8dbcd8cec0 100644
Binary files a/docs/images/provider-added.png and b/docs/images/provider-added.png differ
diff --git a/docs/images/providers/add-account-id.png b/docs/images/providers/add-account-id.png
index bfded597a9..8df1cd41b7 100644
Binary files a/docs/images/providers/add-account-id.png and b/docs/images/providers/add-account-id.png differ
diff --git a/docs/images/providers/add-alibaba-account-id.png b/docs/images/providers/add-alibaba-account-id.png
index 2e49f995cc..7d0c403bcd 100644
Binary files a/docs/images/providers/add-alibaba-account-id.png and b/docs/images/providers/add-alibaba-account-id.png differ
diff --git a/docs/images/providers/add-credentials-azure-prowler-cloud.png b/docs/images/providers/add-credentials-azure-prowler-cloud.png
index 48b722a200..38047c0f1c 100644
Binary files a/docs/images/providers/add-credentials-azure-prowler-cloud.png and b/docs/images/providers/add-credentials-azure-prowler-cloud.png differ
diff --git a/docs/images/providers/add-domain-id.png b/docs/images/providers/add-domain-id.png
index ba1a4cf440..bed15413e8 100644
Binary files a/docs/images/providers/add-domain-id.png and b/docs/images/providers/add-domain-id.png differ
diff --git a/docs/images/providers/add-github-account-id.png b/docs/images/providers/add-github-account-id.png
index 899d772c60..ed306752cc 100644
Binary files a/docs/images/providers/add-github-account-id.png and b/docs/images/providers/add-github-account-id.png differ
diff --git a/docs/images/providers/add-iac-repo.png b/docs/images/providers/add-iac-repo.png
index 31981e7fd1..db9ea2e2f7 100644
Binary files a/docs/images/providers/add-iac-repo.png and b/docs/images/providers/add-iac-repo.png differ
diff --git a/docs/images/providers/add-project-id.png b/docs/images/providers/add-project-id.png
index 41b2971a06..e157e06da0 100644
Binary files a/docs/images/providers/add-project-id.png and b/docs/images/providers/add-project-id.png differ
diff --git a/docs/images/providers/add-subscription-id.png b/docs/images/providers/add-subscription-id.png
index 235fd377b8..bde1471834 100644
Binary files a/docs/images/providers/add-subscription-id.png and b/docs/images/providers/add-subscription-id.png differ
diff --git a/docs/images/providers/alibaba-credentials-form.png b/docs/images/providers/alibaba-credentials-form.png
index 3cefc0253c..67dd88fc2c 100644
Binary files a/docs/images/providers/alibaba-credentials-form.png and b/docs/images/providers/alibaba-credentials-form.png differ
diff --git a/docs/images/providers/alibaba-get-role-arn.png b/docs/images/providers/alibaba-get-role-arn.png
index d95822a18e..5a3f34e201 100644
Binary files a/docs/images/providers/alibaba-get-role-arn.png and b/docs/images/providers/alibaba-get-role-arn.png differ
diff --git a/docs/images/providers/assume-role-overview.png b/docs/images/providers/assume-role-overview.png
index d99ea0564e..cc6e346e2f 100644
Binary files a/docs/images/providers/assume-role-overview.png and b/docs/images/providers/assume-role-overview.png differ
diff --git a/docs/images/providers/auth-github-app.png b/docs/images/providers/auth-github-app.png
index ce7555f081..83ca1c93ef 100644
Binary files a/docs/images/providers/auth-github-app.png and b/docs/images/providers/auth-github-app.png differ
diff --git a/docs/images/providers/auth-oauth.png b/docs/images/providers/auth-oauth.png
index fd38330aa4..9555b6c565 100644
Binary files a/docs/images/providers/auth-oauth.png and b/docs/images/providers/auth-oauth.png differ
diff --git a/docs/images/providers/auth-pat.png b/docs/images/providers/auth-pat.png
index 573640851e..8c416ff6fd 100644
Binary files a/docs/images/providers/auth-pat.png and b/docs/images/providers/auth-pat.png differ
diff --git a/docs/images/providers/certificate-form.png b/docs/images/providers/certificate-form.png
index b19c079d23..d5616afeb6 100644
Binary files a/docs/images/providers/certificate-form.png and b/docs/images/providers/certificate-form.png differ
diff --git a/docs/images/providers/click-next-azure.png b/docs/images/providers/click-next-azure.png
index 4e2e90cd98..9a5b654743 100644
Binary files a/docs/images/providers/click-next-azure.png and b/docs/images/providers/click-next-azure.png differ
diff --git a/docs/images/providers/click-next-m365.png b/docs/images/providers/click-next-m365.png
index 50fed40735..d5f667c2df 100644
Binary files a/docs/images/providers/click-next-m365.png and b/docs/images/providers/click-next-m365.png differ
diff --git a/docs/images/providers/cloudflare-account-id-form.png b/docs/images/providers/cloudflare-account-id-form.png
index 4175c44713..e440139c29 100644
Binary files a/docs/images/providers/cloudflare-account-id-form.png and b/docs/images/providers/cloudflare-account-id-form.png differ
diff --git a/docs/images/providers/cloudflare-api-email-form.png b/docs/images/providers/cloudflare-api-email-form.png
index 7f9526e93c..3bd1f8f4ef 100644
Binary files a/docs/images/providers/cloudflare-api-email-form.png and b/docs/images/providers/cloudflare-api-email-form.png differ
diff --git a/docs/images/providers/cloudflare-auth-selection.png b/docs/images/providers/cloudflare-auth-selection.png
index 4a8610f050..997ba8de4c 100644
Binary files a/docs/images/providers/cloudflare-auth-selection.png and b/docs/images/providers/cloudflare-auth-selection.png differ
diff --git a/docs/images/providers/cloudflare-token-form.png b/docs/images/providers/cloudflare-token-form.png
index a147bcbfe2..e8d5d81fa4 100644
Binary files a/docs/images/providers/cloudflare-token-form.png and b/docs/images/providers/cloudflare-token-form.png differ
diff --git a/docs/images/providers/connect-via-credentials.png b/docs/images/providers/connect-via-credentials.png
index 4e3ba9e7f8..203f601e2e 100644
Binary files a/docs/images/providers/connect-via-credentials.png and b/docs/images/providers/connect-via-credentials.png differ
diff --git a/docs/images/providers/googleworkspace-credentials-form.png b/docs/images/providers/googleworkspace-credentials-form.png
index bc85df96b6..e6410a9fa7 100644
Binary files a/docs/images/providers/googleworkspace-credentials-form.png and b/docs/images/providers/googleworkspace-credentials-form.png differ
diff --git a/docs/images/providers/googleworkspace-customer-id-form.png b/docs/images/providers/googleworkspace-customer-id-form.png
index ced135e5eb..a5f49dc588 100644
Binary files a/docs/images/providers/googleworkspace-customer-id-form.png and b/docs/images/providers/googleworkspace-customer-id-form.png differ
diff --git a/docs/images/providers/iac-authentication.png b/docs/images/providers/iac-authentication.png
index c6caad4ae1..5807ddb573 100644
Binary files a/docs/images/providers/iac-authentication.png and b/docs/images/providers/iac-authentication.png differ
diff --git a/docs/images/providers/iac-verify-connection.png b/docs/images/providers/iac-verify-connection.png
index bc918f6c24..f225363448 100644
Binary files a/docs/images/providers/iac-verify-connection.png and b/docs/images/providers/iac-verify-connection.png differ
diff --git a/docs/images/providers/launch-scan-alibaba.png b/docs/images/providers/launch-scan-alibaba.png
index 324e1334c4..0b210be974 100644
Binary files a/docs/images/providers/launch-scan-alibaba.png and b/docs/images/providers/launch-scan-alibaba.png differ
diff --git a/docs/images/providers/launch-scan-button-prowler-cloud.png b/docs/images/providers/launch-scan-button-prowler-cloud.png
index 06b7e37a85..ec6d3d3784 100644
Binary files a/docs/images/providers/launch-scan-button-prowler-cloud.png and b/docs/images/providers/launch-scan-button-prowler-cloud.png differ
diff --git a/docs/images/providers/launch-scan.png b/docs/images/providers/launch-scan.png
index 07601cf63a..ec6d3d3784 100644
Binary files a/docs/images/providers/launch-scan.png and b/docs/images/providers/launch-scan.png differ
diff --git a/docs/images/providers/m365-auth-selection-form.png b/docs/images/providers/m365-auth-selection-form.png
index 4757f44d96..40992fa38f 100644
Binary files a/docs/images/providers/m365-auth-selection-form.png and b/docs/images/providers/m365-auth-selection-form.png differ
diff --git a/docs/images/providers/next-button-prowler-cloud.png b/docs/images/providers/next-button-prowler-cloud.png
index f41437c17e..26fe233f5d 100644
Binary files a/docs/images/providers/next-button-prowler-cloud.png and b/docs/images/providers/next-button-prowler-cloud.png differ
diff --git a/docs/images/providers/paste-role-arn-prowler.png b/docs/images/providers/paste-role-arn-prowler.png
index 82d7165334..8d9baf91b8 100644
Binary files a/docs/images/providers/paste-role-arn-prowler.png and b/docs/images/providers/paste-role-arn-prowler.png differ
diff --git a/docs/images/providers/prowler-cloud-credentials-next.png b/docs/images/providers/prowler-cloud-credentials-next.png
index 0f73b00905..f8fe6a5659 100644
Binary files a/docs/images/providers/prowler-cloud-credentials-next.png and b/docs/images/providers/prowler-cloud-credentials-next.png differ
diff --git a/docs/images/providers/prowler-cloud-external-id.png b/docs/images/providers/prowler-cloud-external-id.png
index 79fb1b19e3..aa58cf9c09 100644
Binary files a/docs/images/providers/prowler-cloud-external-id.png and b/docs/images/providers/prowler-cloud-external-id.png differ
diff --git a/docs/images/providers/secret-form.png b/docs/images/providers/secret-form.png
index e202c56aab..72dc427f2e 100644
Binary files a/docs/images/providers/secret-form.png and b/docs/images/providers/secret-form.png differ
diff --git a/docs/images/providers/select-alibaba-cloud.png b/docs/images/providers/select-alibaba-cloud.png
index 8b65931472..019a62b9d7 100644
Binary files a/docs/images/providers/select-alibaba-cloud.png and b/docs/images/providers/select-alibaba-cloud.png differ
diff --git a/docs/images/providers/select-auth-method-alibaba.png b/docs/images/providers/select-auth-method-alibaba.png
index ffd0083bf0..215a683bed 100644
Binary files a/docs/images/providers/select-auth-method-alibaba.png and b/docs/images/providers/select-auth-method-alibaba.png differ
diff --git a/docs/images/providers/select-auth-method.png b/docs/images/providers/select-auth-method.png
index cab2c844ce..207c870360 100644
Binary files a/docs/images/providers/select-auth-method.png and b/docs/images/providers/select-auth-method.png differ
diff --git a/docs/images/providers/select-aws.png b/docs/images/providers/select-aws.png
index f7d08ae628..8702b1976d 100644
Binary files a/docs/images/providers/select-aws.png and b/docs/images/providers/select-aws.png differ
diff --git a/docs/images/providers/select-azure-prowler-cloud.png b/docs/images/providers/select-azure-prowler-cloud.png
index 2b8b473d0a..bb53336c0e 100644
Binary files a/docs/images/providers/select-azure-prowler-cloud.png and b/docs/images/providers/select-azure-prowler-cloud.png differ
diff --git a/docs/images/providers/select-cloudflare-prowler-cloud.png b/docs/images/providers/select-cloudflare-prowler-cloud.png
index 508f17d595..df36ae87e3 100644
Binary files a/docs/images/providers/select-cloudflare-prowler-cloud.png and b/docs/images/providers/select-cloudflare-prowler-cloud.png differ
diff --git a/docs/images/providers/select-gcp.png b/docs/images/providers/select-gcp.png
index 0aed14394c..1f9cee49b5 100644
Binary files a/docs/images/providers/select-gcp.png and b/docs/images/providers/select-gcp.png differ
diff --git a/docs/images/providers/select-github.png b/docs/images/providers/select-github.png
index 5208e658d0..65e0e665a4 100644
Binary files a/docs/images/providers/select-github.png and b/docs/images/providers/select-github.png differ
diff --git a/docs/images/providers/select-googleworkspace-prowler-cloud.png b/docs/images/providers/select-googleworkspace-prowler-cloud.png
index b8a83b6e83..0802a0562a 100644
Binary files a/docs/images/providers/select-googleworkspace-prowler-cloud.png and b/docs/images/providers/select-googleworkspace-prowler-cloud.png differ
diff --git a/docs/images/providers/select-iac.png b/docs/images/providers/select-iac.png
index 1dc474cbe8..6fedc1dcd3 100644
Binary files a/docs/images/providers/select-iac.png and b/docs/images/providers/select-iac.png differ
diff --git a/docs/images/providers/select-m365-prowler-cloud.png b/docs/images/providers/select-m365-prowler-cloud.png
index 6507c89839..1424cadb1a 100644
Binary files a/docs/images/providers/select-m365-prowler-cloud.png and b/docs/images/providers/select-m365-prowler-cloud.png differ
diff --git a/docs/images/providers/select-vercel-prowler-cloud.png b/docs/images/providers/select-vercel-prowler-cloud.png
index b332103e1f..40f5d774c7 100644
Binary files a/docs/images/providers/select-vercel-prowler-cloud.png and b/docs/images/providers/select-vercel-prowler-cloud.png differ
diff --git a/docs/images/providers/vercel-team-id-form.png b/docs/images/providers/vercel-team-id-form.png
index fad53fe017..d1e6185800 100644
Binary files a/docs/images/providers/vercel-team-id-form.png and b/docs/images/providers/vercel-team-id-form.png differ
diff --git a/docs/images/providers/vercel-token-form.png b/docs/images/providers/vercel-token-form.png
index 991b9ddedc..136b0cce78 100644
Binary files a/docs/images/providers/vercel-token-form.png and b/docs/images/providers/vercel-token-form.png differ
diff --git a/docs/images/prowler-app/add-cloud-provider.png b/docs/images/prowler-app/add-cloud-provider.png
index d8f19b2054..4dc0749a27 100644
Binary files a/docs/images/prowler-app/add-cloud-provider.png and b/docs/images/prowler-app/add-cloud-provider.png differ
diff --git a/docs/images/prowler-app/alerts/alerts-list.png b/docs/images/prowler-app/alerts/alerts-list.png
index 7bb03415fa..153f5d6b24 100644
Binary files a/docs/images/prowler-app/alerts/alerts-list.png and b/docs/images/prowler-app/alerts/alerts-list.png differ
diff --git a/docs/images/prowler-app/alerts/create-alert-from-findings.png b/docs/images/prowler-app/alerts/create-alert-from-findings.png
index 5524389877..845aa627d3 100644
Binary files a/docs/images/prowler-app/alerts/create-alert-from-findings.png and b/docs/images/prowler-app/alerts/create-alert-from-findings.png differ
diff --git a/docs/images/prowler-app/alerts/create-alert-modal.png b/docs/images/prowler-app/alerts/create-alert-modal.png
index 54924638af..46ef36b690 100644
Binary files a/docs/images/prowler-app/alerts/create-alert-modal.png and b/docs/images/prowler-app/alerts/create-alert-modal.png differ
diff --git a/docs/images/prowler-app/alerts/edit-alert-test.png b/docs/images/prowler-app/alerts/edit-alert-test.png
index 252a0cf67c..2047792ca4 100644
Binary files a/docs/images/prowler-app/alerts/edit-alert-test.png and b/docs/images/prowler-app/alerts/edit-alert-test.png differ
diff --git a/docs/images/prowler-app/attack-paths/query-no-data.png b/docs/images/prowler-app/attack-paths/query-no-data.png
new file mode 100644
index 0000000000..33de2b9d3d
Binary files /dev/null and b/docs/images/prowler-app/attack-paths/query-no-data.png differ
diff --git a/docs/images/prowler-app/attack-paths/query-selector-hidden-empty.png b/docs/images/prowler-app/attack-paths/query-selector-hidden-empty.png
new file mode 100644
index 0000000000..9f4fbd122d
Binary files /dev/null and b/docs/images/prowler-app/attack-paths/query-selector-hidden-empty.png differ
diff --git a/docs/images/prowler-app/cloud-providers-page.png b/docs/images/prowler-app/cloud-providers-page.png
index dcbce73a10..6a81a9d0c1 100644
Binary files a/docs/images/prowler-app/cloud-providers-page.png and b/docs/images/prowler-app/cloud-providers-page.png differ
diff --git a/docs/images/prowler-app/gcp-auth-methods.png b/docs/images/prowler-app/gcp-auth-methods.png
index dc8681396e..ac5f2e5c0e 100644
Binary files a/docs/images/prowler-app/gcp-auth-methods.png and b/docs/images/prowler-app/gcp-auth-methods.png differ
diff --git a/docs/images/prowler-app/gcp-service-account-creds.png b/docs/images/prowler-app/gcp-service-account-creds.png
index af09776ab1..6d80ea5ca0 100644
Binary files a/docs/images/prowler-app/gcp-service-account-creds.png and b/docs/images/prowler-app/gcp-service-account-creds.png differ
diff --git a/docs/images/prowler-app/jira/connection-settings.png b/docs/images/prowler-app/jira/connection-settings.png
index 86ebe73dea..2ec60dfe0b 100644
Binary files a/docs/images/prowler-app/jira/connection-settings.png and b/docs/images/prowler-app/jira/connection-settings.png differ
diff --git a/docs/images/prowler-app/jira/group-info.png b/docs/images/prowler-app/jira/group-info.png
new file mode 100644
index 0000000000..045087d4e9
Binary files /dev/null and b/docs/images/prowler-app/jira/group-info.png differ
diff --git a/docs/images/prowler-app/jira/group-resources.png b/docs/images/prowler-app/jira/group-resources.png
new file mode 100644
index 0000000000..8adedc65d0
Binary files /dev/null and b/docs/images/prowler-app/jira/group-resources.png differ
diff --git a/docs/images/prowler-app/jira/integrations-tab.png b/docs/images/prowler-app/jira/integrations-tab.png
index e71773fc52..11c30c4806 100644
Binary files a/docs/images/prowler-app/jira/integrations-tab.png and b/docs/images/prowler-app/jira/integrations-tab.png differ
diff --git a/docs/images/prowler-app/jira/prowler-finding-group.png b/docs/images/prowler-app/jira/prowler-finding-group.png
new file mode 100644
index 0000000000..1967d44875
Binary files /dev/null and b/docs/images/prowler-app/jira/prowler-finding-group.png differ
diff --git a/docs/images/prowler-app/jira/select-group.png b/docs/images/prowler-app/jira/select-group.png
new file mode 100644
index 0000000000..8158ce1c21
Binary files /dev/null and b/docs/images/prowler-app/jira/select-group.png differ
diff --git a/docs/images/prowler-app/jira/select-multiple-findings.png b/docs/images/prowler-app/jira/select-multiple-findings.png
new file mode 100644
index 0000000000..d82abc2b5b
Binary files /dev/null and b/docs/images/prowler-app/jira/select-multiple-findings.png differ
diff --git a/docs/images/prowler-app/jira/send-group-to-jira.png b/docs/images/prowler-app/jira/send-group-to-jira.png
new file mode 100644
index 0000000000..4822b83f12
Binary files /dev/null and b/docs/images/prowler-app/jira/send-group-to-jira.png differ
diff --git a/docs/images/prowler-app/jira/send-to-jira-modal.png b/docs/images/prowler-app/jira/send-to-jira-modal.png
index 2cf498926a..dc9b6f990e 100644
Binary files a/docs/images/prowler-app/jira/send-to-jira-modal.png and b/docs/images/prowler-app/jira/send-to-jira-modal.png differ
diff --git a/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-closed.png b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-closed.png
new file mode 100644
index 0000000000..c36da34a95
Binary files /dev/null and b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-closed.png differ
diff --git a/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-context-aware.png b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-context-aware.png
new file mode 100644
index 0000000000..f403943f72
Binary files /dev/null and b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-context-aware.png differ
diff --git a/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-open.png b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-open.png
new file mode 100644
index 0000000000..fd10f5a00b
Binary files /dev/null and b/docs/images/prowler-app/lighthouse/prowler-cloud/side-panel-open.png differ
diff --git a/docs/images/prowler-app/multi-tenant/create-organization-button.png b/docs/images/prowler-app/multi-tenant/create-organization-button.png
index 70675c334f..ad62ce339e 100644
Binary files a/docs/images/prowler-app/multi-tenant/create-organization-button.png and b/docs/images/prowler-app/multi-tenant/create-organization-button.png differ
diff --git a/docs/images/prowler-app/multi-tenant/create-organization-modal.png b/docs/images/prowler-app/multi-tenant/create-organization-modal.png
index f0f932035c..30f953dfe1 100644
Binary files a/docs/images/prowler-app/multi-tenant/create-organization-modal.png and b/docs/images/prowler-app/multi-tenant/create-organization-modal.png differ
diff --git a/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png b/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png
index 41f5231753..cc4aa4c404 100644
Binary files a/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png and b/docs/images/prowler-app/multi-tenant/delete-active-organization-modal.png differ
diff --git a/docs/images/prowler-app/multi-tenant/delete-organization-modal.png b/docs/images/prowler-app/multi-tenant/delete-organization-modal.png
index dd06f937e9..88265ab766 100644
Binary files a/docs/images/prowler-app/multi-tenant/delete-organization-modal.png and b/docs/images/prowler-app/multi-tenant/delete-organization-modal.png differ
diff --git a/docs/images/prowler-app/multi-tenant/edit-organization-modal.png b/docs/images/prowler-app/multi-tenant/edit-organization-modal.png
index e0d28c727d..ef23c04c03 100644
Binary files a/docs/images/prowler-app/multi-tenant/edit-organization-modal.png and b/docs/images/prowler-app/multi-tenant/edit-organization-modal.png differ
diff --git a/docs/images/prowler-app/multi-tenant/expel-user-organization-modal.png b/docs/images/prowler-app/multi-tenant/expel-user-organization-modal.png
index ed3b190c61..7a9b851295 100644
Binary files a/docs/images/prowler-app/multi-tenant/expel-user-organization-modal.png and b/docs/images/prowler-app/multi-tenant/expel-user-organization-modal.png differ
diff --git a/docs/images/prowler-app/multi-tenant/expel-user-organization.png b/docs/images/prowler-app/multi-tenant/expel-user-organization.png
index 09f72e04ea..11f270b8b9 100644
Binary files a/docs/images/prowler-app/multi-tenant/expel-user-organization.png and b/docs/images/prowler-app/multi-tenant/expel-user-organization.png differ
diff --git a/docs/images/prowler-app/multi-tenant/organizations-card.png b/docs/images/prowler-app/multi-tenant/organizations-card.png
index 10ddb4b15b..f401bac7ab 100644
Binary files a/docs/images/prowler-app/multi-tenant/organizations-card.png and b/docs/images/prowler-app/multi-tenant/organizations-card.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_details.png b/docs/images/prowler-app/rbac/invitation_details.png
index 656a698308..f2d2169508 100644
Binary files a/docs/images/prowler-app/rbac/invitation_details.png and b/docs/images/prowler-app/rbac/invitation_details.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_details_1.png b/docs/images/prowler-app/rbac/invitation_details_1.png
index e167db74af..7a51736cbe 100644
Binary files a/docs/images/prowler-app/rbac/invitation_details_1.png and b/docs/images/prowler-app/rbac/invitation_details_1.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_edit.png b/docs/images/prowler-app/rbac/invitation_edit.png
index ef3d81f192..d256d0593c 100644
Binary files a/docs/images/prowler-app/rbac/invitation_edit.png and b/docs/images/prowler-app/rbac/invitation_edit.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_edit_1.png b/docs/images/prowler-app/rbac/invitation_edit_1.png
index 6d1a1d2223..53761025ed 100644
Binary files a/docs/images/prowler-app/rbac/invitation_edit_1.png and b/docs/images/prowler-app/rbac/invitation_edit_1.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_info.png b/docs/images/prowler-app/rbac/invitation_info.png
index a6ec05f976..1fbbf14c62 100644
Binary files a/docs/images/prowler-app/rbac/invitation_info.png and b/docs/images/prowler-app/rbac/invitation_info.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_revoke.png b/docs/images/prowler-app/rbac/invitation_revoke.png
index 6c4e042c16..ab83853501 100644
Binary files a/docs/images/prowler-app/rbac/invitation_revoke.png and b/docs/images/prowler-app/rbac/invitation_revoke.png differ
diff --git a/docs/images/prowler-app/rbac/invitation_sign-up.png b/docs/images/prowler-app/rbac/invitation_sign-up.png
index f5b67a7762..c4e040f9f1 100644
Binary files a/docs/images/prowler-app/rbac/invitation_sign-up.png and b/docs/images/prowler-app/rbac/invitation_sign-up.png differ
diff --git a/docs/images/prowler-app/rbac/invite.png b/docs/images/prowler-app/rbac/invite.png
index dd60aba314..54cbc27d6b 100644
Binary files a/docs/images/prowler-app/rbac/invite.png and b/docs/images/prowler-app/rbac/invite.png differ
diff --git a/docs/images/prowler-app/rbac/provider_group.png b/docs/images/prowler-app/rbac/provider_group.png
index 878306e02f..8698cc9c41 100644
Binary files a/docs/images/prowler-app/rbac/provider_group.png and b/docs/images/prowler-app/rbac/provider_group.png differ
diff --git a/docs/images/prowler-app/rbac/provider_group_edit.png b/docs/images/prowler-app/rbac/provider_group_edit.png
index 5de9649578..3d37b329b7 100644
Binary files a/docs/images/prowler-app/rbac/provider_group_edit.png and b/docs/images/prowler-app/rbac/provider_group_edit.png differ
diff --git a/docs/images/prowler-app/rbac/provider_group_edit_1.png b/docs/images/prowler-app/rbac/provider_group_edit_1.png
index ed4a090eed..4369983a20 100644
Binary files a/docs/images/prowler-app/rbac/provider_group_edit_1.png and b/docs/images/prowler-app/rbac/provider_group_edit_1.png differ
diff --git a/docs/images/prowler-app/rbac/provider_group_remove.png b/docs/images/prowler-app/rbac/provider_group_remove.png
index 580a8533cf..f504617159 100644
Binary files a/docs/images/prowler-app/rbac/provider_group_remove.png and b/docs/images/prowler-app/rbac/provider_group_remove.png differ
diff --git a/docs/images/prowler-app/rbac/role_create_1.png b/docs/images/prowler-app/rbac/role_create_1.png
index a96b0ac9ef..b88fe990db 100644
Binary files a/docs/images/prowler-app/rbac/role_create_1.png and b/docs/images/prowler-app/rbac/role_create_1.png differ
diff --git a/docs/images/prowler-app/rbac/user_edit.png b/docs/images/prowler-app/rbac/user_edit.png
index 421ea93156..1fab60d182 100644
Binary files a/docs/images/prowler-app/rbac/user_edit.png and b/docs/images/prowler-app/rbac/user_edit.png differ
diff --git a/docs/images/prowler-app/rbac/user_edit_details.png b/docs/images/prowler-app/rbac/user_edit_details.png
index 3e4734f142..7744f0472a 100644
Binary files a/docs/images/prowler-app/rbac/user_edit_details.png and b/docs/images/prowler-app/rbac/user_edit_details.png differ
diff --git a/docs/images/prowler-app/rbac/user_remove.png b/docs/images/prowler-app/rbac/user_remove.png
index 4c368ded66..11f270b8b9 100644
Binary files a/docs/images/prowler-app/rbac/user_remove.png and b/docs/images/prowler-app/rbac/user_remove.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-1.png b/docs/images/prowler-app/s3/s3-integration-ui-1.png
index 1081f20453..6c60831843 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-1.png and b/docs/images/prowler-app/s3/s3-integration-ui-1.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-2.png b/docs/images/prowler-app/s3/s3-integration-ui-2.png
index 618621ce26..019c29675b 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-2.png and b/docs/images/prowler-app/s3/s3-integration-ui-2.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-3.png b/docs/images/prowler-app/s3/s3-integration-ui-3.png
index 6fabbe2ed3..a82a3109db 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-3.png and b/docs/images/prowler-app/s3/s3-integration-ui-3.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-4.png b/docs/images/prowler-app/s3/s3-integration-ui-4.png
index 1967eda8af..867ac7d48d 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-4.png and b/docs/images/prowler-app/s3/s3-integration-ui-4.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-5.png b/docs/images/prowler-app/s3/s3-integration-ui-5.png
index ffdea752f1..4f1e9e7ebf 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-5.png and b/docs/images/prowler-app/s3/s3-integration-ui-5.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-6.png b/docs/images/prowler-app/s3/s3-integration-ui-6.png
index 330588e17b..eeb43679ef 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-6.png and b/docs/images/prowler-app/s3/s3-integration-ui-6.png differ
diff --git a/docs/images/prowler-app/s3/s3-integration-ui-7.png b/docs/images/prowler-app/s3/s3-integration-ui-7.png
index 3448f82a48..dd0e155d46 100644
Binary files a/docs/images/prowler-app/s3/s3-integration-ui-7.png and b/docs/images/prowler-app/s3/s3-integration-ui-7.png differ
diff --git a/docs/images/prowler-app/saml/saml-multiple-domains.png b/docs/images/prowler-app/saml/saml-multiple-domains.png
new file mode 100644
index 0000000000..404afa046d
Binary files /dev/null and b/docs/images/prowler-app/saml/saml-multiple-domains.png differ
diff --git a/docs/images/prowler-app/saml/saml-signin-1.png b/docs/images/prowler-app/saml/saml-signin-1.png
index 5da2e84711..35522436bb 100644
Binary files a/docs/images/prowler-app/saml/saml-signin-1.png and b/docs/images/prowler-app/saml/saml-signin-1.png differ
diff --git a/docs/images/prowler-app/saml/saml-signin-2.png b/docs/images/prowler-app/saml/saml-signin-2.png
index f0f7082fc9..9245d36f8b 100644
Binary files a/docs/images/prowler-app/saml/saml-signin-2.png and b/docs/images/prowler-app/saml/saml-signin-2.png differ
diff --git a/docs/images/prowler-app/saml/saml-sso-enabled.png b/docs/images/prowler-app/saml/saml-sso-enabled.png
new file mode 100644
index 0000000000..6c705ba00c
Binary files /dev/null and b/docs/images/prowler-app/saml/saml-sso-enabled.png differ
diff --git a/docs/images/prowler-app/saml/saml-sso-remove.png b/docs/images/prowler-app/saml/saml-sso-remove.png
new file mode 100644
index 0000000000..dd61a26cd1
Binary files /dev/null and b/docs/images/prowler-app/saml/saml-sso-remove.png differ
diff --git a/docs/images/prowler-app/saml/saml-step-1.png b/docs/images/prowler-app/saml/saml-step-1.png
index d505c2597c..694e17ca27 100644
Binary files a/docs/images/prowler-app/saml/saml-step-1.png and b/docs/images/prowler-app/saml/saml-step-1.png differ
diff --git a/docs/images/prowler-app/saml/saml-step-2.png b/docs/images/prowler-app/saml/saml-step-2.png
index ddb036c98d..3bcb664701 100644
Binary files a/docs/images/prowler-app/saml/saml-step-2.png and b/docs/images/prowler-app/saml/saml-step-2.png differ
diff --git a/docs/images/prowler-app/saml/saml-step-3.png b/docs/images/prowler-app/saml/saml-step-3.png
index 2b8dfbd845..188e8c9584 100644
Binary files a/docs/images/prowler-app/saml/saml-step-3.png and b/docs/images/prowler-app/saml/saml-step-3.png differ
diff --git a/docs/images/prowler-app/saml/saml-step-4.png b/docs/images/prowler-app/saml/saml-step-4.png
deleted file mode 100644
index cca0987c50..0000000000
Binary files a/docs/images/prowler-app/saml/saml-step-4.png and /dev/null differ
diff --git a/docs/images/prowler-app/saml/saml-step-remove.png b/docs/images/prowler-app/saml/saml-step-remove.png
deleted file mode 100644
index f0868691af..0000000000
Binary files a/docs/images/prowler-app/saml/saml-step-remove.png and /dev/null differ
diff --git a/docs/images/prowler-app/security-hub/create-integration.png b/docs/images/prowler-app/security-hub/create-integration.png
index 145e68d201..dbafd21672 100644
Binary files a/docs/images/prowler-app/security-hub/create-integration.png and b/docs/images/prowler-app/security-hub/create-integration.png differ
diff --git a/docs/images/prowler-app/security-hub/integration-settings.png b/docs/images/prowler-app/security-hub/integration-settings.png
index 3a32bf0830..3201904c23 100644
Binary files a/docs/images/prowler-app/security-hub/integration-settings.png and b/docs/images/prowler-app/security-hub/integration-settings.png differ
diff --git a/docs/images/prowler-app/security-hub/integrations-tab.png b/docs/images/prowler-app/security-hub/integrations-tab.png
index 9d11cae33a..af4178b964 100644
Binary files a/docs/images/prowler-app/security-hub/integrations-tab.png and b/docs/images/prowler-app/security-hub/integrations-tab.png differ
diff --git a/docs/images/prowler-app/social-login/social_login_buttons.png b/docs/images/prowler-app/social-login/social_login_buttons.png
index 476081e0fc..9adaaac790 100644
Binary files a/docs/images/prowler-app/social-login/social_login_buttons.png and b/docs/images/prowler-app/social-login/social_login_buttons.png differ
diff --git a/docs/images/prowler-app/social-login/social_login_buttons_disabled.png b/docs/images/prowler-app/social-login/social_login_buttons_disabled.png
index 7b11a7802d..fd46318b65 100644
Binary files a/docs/images/prowler-app/social-login/social_login_buttons_disabled.png and b/docs/images/prowler-app/social-login/social_login_buttons_disabled.png differ
diff --git a/docs/images/prowler-for-msps/add-customer-billing.png b/docs/images/prowler-for-msps/add-customer-billing.png
new file mode 100644
index 0000000000..674c85c697
Binary files /dev/null and b/docs/images/prowler-for-msps/add-customer-billing.png differ
diff --git a/docs/images/prowler-for-msps/add-customer-launch.png b/docs/images/prowler-for-msps/add-customer-launch.png
new file mode 100644
index 0000000000..1ea41ff7fb
Binary files /dev/null and b/docs/images/prowler-for-msps/add-customer-launch.png differ
diff --git a/docs/images/prowler-for-msps/add-customer-profile.png b/docs/images/prowler-for-msps/add-customer-profile.png
new file mode 100644
index 0000000000..ae3290a18c
Binary files /dev/null and b/docs/images/prowler-for-msps/add-customer-profile.png differ
diff --git a/docs/images/prowler-for-msps/change-plan-dialog.png b/docs/images/prowler-for-msps/change-plan-dialog.png
new file mode 100644
index 0000000000..5b806bb512
Binary files /dev/null and b/docs/images/prowler-for-msps/change-plan-dialog.png differ
diff --git a/docs/images/prowler-for-msps/customer-row-actions.png b/docs/images/prowler-for-msps/customer-row-actions.png
new file mode 100644
index 0000000000..8fe86728e1
Binary files /dev/null and b/docs/images/prowler-for-msps/customer-row-actions.png differ
diff --git a/docs/images/prowler-for-msps/customers-table.png b/docs/images/prowler-for-msps/customers-table.png
new file mode 100644
index 0000000000..d636b02704
Binary files /dev/null and b/docs/images/prowler-for-msps/customers-table.png differ
diff --git a/docs/images/prowler-for-msps/dashboard.png b/docs/images/prowler-for-msps/dashboard.png
new file mode 100644
index 0000000000..615b976b86
Binary files /dev/null and b/docs/images/prowler-for-msps/dashboard.png differ
diff --git a/docs/images/prowler-for-msps/invite-user-dialog.png b/docs/images/prowler-for-msps/invite-user-dialog.png
new file mode 100644
index 0000000000..24e16acd4a
Binary files /dev/null and b/docs/images/prowler-for-msps/invite-user-dialog.png differ
diff --git a/docs/images/prowler-for-msps/settings-branding.png b/docs/images/prowler-for-msps/settings-branding.png
new file mode 100644
index 0000000000..3d481a9d77
Binary files /dev/null and b/docs/images/prowler-for-msps/settings-branding.png differ
diff --git a/docs/images/prowler-for-msps/settings-profile.png b/docs/images/prowler-for-msps/settings-profile.png
new file mode 100644
index 0000000000..8749b66b1b
Binary files /dev/null and b/docs/images/prowler-for-msps/settings-profile.png differ
diff --git a/docs/images/prowler-for-msps/sign-in-form.png b/docs/images/prowler-for-msps/sign-in-form.png
new file mode 100644
index 0000000000..6f9a0ce64d
Binary files /dev/null and b/docs/images/prowler-for-msps/sign-in-form.png differ
diff --git a/docs/images/prowler-for-msps/sign-up-form.png b/docs/images/prowler-for-msps/sign-up-form.png
new file mode 100644
index 0000000000..78fddccf28
Binary files /dev/null and b/docs/images/prowler-for-msps/sign-up-form.png differ
diff --git a/docs/images/prowler-mcp/claude/claude-code-mcp-add.png b/docs/images/prowler-mcp/claude/claude-code-mcp-add.png
new file mode 100644
index 0000000000..2f5894219c
Binary files /dev/null and b/docs/images/prowler-mcp/claude/claude-code-mcp-add.png differ
diff --git a/docs/images/prowler-mcp/claude/claude-code-mcp-command.png b/docs/images/prowler-mcp/claude/claude-code-mcp-command.png
new file mode 100644
index 0000000000..c036ce8b61
Binary files /dev/null and b/docs/images/prowler-mcp/claude/claude-code-mcp-command.png differ
diff --git a/docs/images/prowler-mcp/claude/claude-code-prowler-query.png b/docs/images/prowler-mcp/claude/claude-code-prowler-query.png
new file mode 100644
index 0000000000..f78f5286e6
Binary files /dev/null and b/docs/images/prowler-mcp/claude/claude-code-prowler-query.png differ
diff --git a/docs/images/prowler-mcp/claude/claude-desktop-developer-settings.png b/docs/images/prowler-mcp/claude/claude-desktop-developer-settings.png
new file mode 100644
index 0000000000..494661238b
Binary files /dev/null and b/docs/images/prowler-mcp/claude/claude-desktop-developer-settings.png differ
diff --git a/docs/images/prowler-mcp/claude/claude-desktop-prowler-tools.png b/docs/images/prowler-mcp/claude/claude-desktop-prowler-tools.png
new file mode 100644
index 0000000000..30d0ad7be8
Binary files /dev/null and b/docs/images/prowler-mcp/claude/claude-desktop-prowler-tools.png differ
diff --git a/docs/images/prowler-mcp/codex/codex-app-mcp-servers.png b/docs/images/prowler-mcp/codex/codex-app-mcp-servers.png
new file mode 100644
index 0000000000..3b735864f9
Binary files /dev/null and b/docs/images/prowler-mcp/codex/codex-app-mcp-servers.png differ
diff --git a/docs/images/prowler-mcp/codex/codex-mcp-slash-command.png b/docs/images/prowler-mcp/codex/codex-mcp-slash-command.png
new file mode 100644
index 0000000000..df3f8a65b3
Binary files /dev/null and b/docs/images/prowler-mcp/codex/codex-mcp-slash-command.png differ
diff --git a/docs/images/prowler-mcp/codex/codex-prowler-query.png b/docs/images/prowler-mcp/codex/codex-prowler-query.png
new file mode 100644
index 0000000000..b225933cfd
Binary files /dev/null and b/docs/images/prowler-mcp/codex/codex-prowler-query.png differ
diff --git a/docs/images/prowler-mcp/cursor/cursor-customize-page.png b/docs/images/prowler-mcp/cursor/cursor-customize-page.png
new file mode 100644
index 0000000000..8afe41c1c5
Binary files /dev/null and b/docs/images/prowler-mcp/cursor/cursor-customize-page.png differ
diff --git a/docs/images/prowler-mcp/cursor/cursor-mcp-json.png b/docs/images/prowler-mcp/cursor/cursor-mcp-json.png
new file mode 100644
index 0000000000..79814b4db4
Binary files /dev/null and b/docs/images/prowler-mcp/cursor/cursor-mcp-json.png differ
diff --git a/docs/images/prowler-mcp/cursor/cursor-prowler-connected.png b/docs/images/prowler-mcp/cursor/cursor-prowler-connected.png
new file mode 100644
index 0000000000..ae946abdc5
Binary files /dev/null and b/docs/images/prowler-mcp/cursor/cursor-prowler-connected.png differ
diff --git a/docs/images/prowler-mcp/cursor/cursor-prowler-query.png b/docs/images/prowler-mcp/cursor/cursor-prowler-query.png
new file mode 100644
index 0000000000..3bdec29a36
Binary files /dev/null and b/docs/images/prowler-mcp/cursor/cursor-prowler-query.png differ
diff --git a/docs/images/prowler-mcp/vscode/vscode-agent-tools.png b/docs/images/prowler-mcp/vscode/vscode-agent-tools.png
new file mode 100644
index 0000000000..e1d90719d6
Binary files /dev/null and b/docs/images/prowler-mcp/vscode/vscode-agent-tools.png differ
diff --git a/docs/images/prowler-mcp/vscode/vscode-command-palette.png b/docs/images/prowler-mcp/vscode/vscode-command-palette.png
new file mode 100644
index 0000000000..453e973a36
Binary files /dev/null and b/docs/images/prowler-mcp/vscode/vscode-command-palette.png differ
diff --git a/docs/images/prowler-mcp/vscode/vscode-list-servers.png b/docs/images/prowler-mcp/vscode/vscode-list-servers.png
new file mode 100644
index 0000000000..596a6af443
Binary files /dev/null and b/docs/images/prowler-mcp/vscode/vscode-list-servers.png differ
diff --git a/docs/images/prowler-mcp/vscode/vscode-mcp-json.png b/docs/images/prowler-mcp/vscode/vscode-mcp-json.png
new file mode 100644
index 0000000000..01fbf91768
Binary files /dev/null and b/docs/images/prowler-mcp/vscode/vscode-mcp-json.png differ
diff --git a/docs/images/prowler_mcp_schema.mmd b/docs/images/prowler_mcp_schema.mmd
index 96973546f6..251d3651be 100644
--- a/docs/images/prowler_mcp_schema.mmd
+++ b/docs/images/prowler_mcp_schema.mmd
@@ -1,29 +1,43 @@
flowchart LR
- subgraph HOSTS["MCP Hosts"]
+ subgraph HOSTS["MCP Clients"]
chat["Chat Interfaces (Claude Desktop, LobeChat)"]
ide["IDEs and Code Editors (Claude Code, Cursor)"]
apps["Other AI Applications (5ire, custom agents)"]
end
- subgraph MCP["Prowler MCP Server"]
- app_tools["prowler_app_* tools (JWT or API key auth) Findings · Providers · Scans Resources · Muting · Compliance Attack Paths"]
+ subgraph SERVERS["Prowler MCP Server"]
+ direction TB
+ cloud["Cloud MCP Server (Recommended) mcp.prowler.com/mcp · HTTP Managed by Prowler · always up to date Adds the Cloud-only prowler_cloud_* tools"]
+ local["Local MCP Server Self-run · STDIO or HTTP Python 3.12+ or Docker You manage updates"]
+ end
+
+ subgraph TOOLS["Prowler MCP Tools"]
+ prowler_tools["prowler_* tools (API key or JWT auth) Findings · Finding Groups · Providers Scans · Resources · Muting · Compliance Attack Paths · Integrations · Users · Roles"]
+ cloud_tools["prowler_cloud_* tools (API key or JWT auth · Cloud only) Alerts · Findings Triage Scan Scheduling · Scan Configurations"]
hub_tools["prowler_hub_* tools (no auth) Checks Catalog · Check Code Fixers · Compliance Frameworks"]
docs_tools["prowler_docs_* tools (no auth) Search · Document Retrieval"]
end
- api["Prowler API (REST)"]
+ api["Prowler API (REST) Cloud · Private Cloud · Local Server"]
hub["hub.prowler.com (REST)"]
docs["docs.prowler.com (Mintlify)"]
- chat -->|STDIO or HTTP| app_tools
- chat -->|STDIO or HTTP| hub_tools
- chat -->|STDIO or HTTP| docs_tools
- ide -->|STDIO or HTTP| app_tools
- ide -->|STDIO or HTTP| hub_tools
- ide -->|STDIO or HTTP| docs_tools
- apps -->|STDIO or HTTP| app_tools
- apps -->|STDIO or HTTP| hub_tools
- apps -->|STDIO or HTTP| docs_tools
- app_tools -->|REST| api
+ chat -->|HTTP| cloud
+ ide -->|HTTP| cloud
+ apps -->|HTTP| cloud
+ chat -->|STDIO or HTTP| local
+ ide -->|STDIO or HTTP| local
+ apps -->|STDIO or HTTP| local
+
+ cloud --> prowler_tools
+ cloud --> cloud_tools
+ cloud --> hub_tools
+ cloud --> docs_tools
+ local --> prowler_tools
+ local --> hub_tools
+ local --> docs_tools
+
+ prowler_tools -->|REST| api
+ cloud_tools -->|REST| api
hub_tools -->|REST| hub
docs_tools -->|REST| docs
diff --git a/docs/images/prowler_mcp_schema.png b/docs/images/prowler_mcp_schema.png
deleted file mode 100644
index 8a8884fa5e..0000000000
Binary files a/docs/images/prowler_mcp_schema.png and /dev/null differ
diff --git a/docs/images/scan-progress.png b/docs/images/scan-progress.png
index 3378775dcd..dd57820410 100644
Binary files a/docs/images/scan-progress.png and b/docs/images/scan-progress.png differ
diff --git a/docs/images/select-provider.png b/docs/images/select-provider.png
index 3151bfe5e7..95607f8657 100644
Binary files a/docs/images/select-provider.png and b/docs/images/select-provider.png differ
diff --git a/docs/images/sign-up-button.png b/docs/images/sign-up-button.png
index b7006e72e5..475d00d81e 100644
Binary files a/docs/images/sign-up-button.png and b/docs/images/sign-up-button.png differ
diff --git a/docs/images/sign-up.png b/docs/images/sign-up.png
index 56e8901b24..8fa20ebf96 100644
Binary files a/docs/images/sign-up.png and b/docs/images/sign-up.png differ
diff --git a/docs/images/test-connection-button.png b/docs/images/test-connection-button.png
index 261cb035c7..38bd5c8a7c 100644
Binary files a/docs/images/test-connection-button.png and b/docs/images/test-connection-button.png differ
diff --git a/docs/introduction.mdx b/docs/introduction.mdx
index 147b4bd184..b21da201cf 100644
--- a/docs/introduction.mdx
+++ b/docs/introduction.mdx
@@ -5,22 +5,37 @@ description: 'Prowler is an open-source cloud security platform that automates s
# What is Prowler?
-**Prowler** is the world’s most widely used open-source cloud security platform that **automates security and compliance** across any cloud environment. With hundreds of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler delivers AI-driven, customizable, and easy-to-use monitoring and integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
+**Prowler** is the world’s most widely used open-source cloud security platform that **automates security and compliance** across any cloud environment. With thousands of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler delivers AI-driven, customizable, and easy-to-use monitoring and integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.

+Prowler ships two product families: Prowler Products, operated or licensed by the Prowler team, and Open Source projects, free to run and extend. See [Prowler product families](/getting-started/products) for every official name, including former names.
+
+### Prowler Products
+
-
- Command Line Interface
-
-
- Web Application
-
-
- A managed service built on top of Prowler App.
+
+ Managed cloud security platform operated by the Prowler team.
- A public library of versioned checks, cloud service artifacts, and compliance frameworks.
+ Free public library of versioned checks, cloud service artifacts, and compliance frameworks.
+
+
+ MCP server that connects AI assistants and agents to Prowler.
+
+
+ Prowler Private Cloud, Prowler Lighthouse AI, and more.
+
+
+
+### Open Source
+
+
+
+ Command line tool to run security scans across all supported providers.
+
+
+ Self-hosted web application and API.
@@ -38,6 +53,7 @@ Prowler supports a wide range of providers organized by category:
| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI |
| [E2E Networks](/user-guide/providers/e2enetworks/getting-started-e2enetworks) | [Contact us](https://prowler.com/contact) | Projects | CLI |
| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI |
+| [Huawei Cloud](/user-guide/providers/huaweicloud/getting-started-huaweicloud) | [Contact us](https://prowler.com/contact) | Accounts | 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 |
@@ -73,9 +89,9 @@ Prowler supports a wide range of providers organized by category:
| Provider | Support | Audit Scope/Entities | Interface |
| ------------------------------------------------------------------- | -------- | -------------------- | --------- |
-| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images / Registries | CLI, API |
+| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images / Registries | UI, API, CLI |
-### Custom Providers (Prowler Cloud Enterprise Only)
+### Custom Providers (Prowler Private Cloud Only)
| Provider | Support | Audit Scope/Entities | Interface |
| -------------------- | -------- | -------------------- | --------- |
@@ -83,7 +99,7 @@ Prowler supports a wide range of providers organized by category:
For more information about the checks and compliance of each provider, visit [Prowler Hub](https://hub.prowler.com).
-## Where to go next?
+## Where to Go Next?
diff --git a/docs/scripts/generate_provider_cards.py b/docs/scripts/generate_provider_cards.py
index 3b02574978..1b35755507 100644
--- a/docs/scripts/generate_provider_cards.py
+++ b/docs/scripts/generate_provider_cards.py
@@ -2,7 +2,7 @@
"""Generate docs/snippets/provider-cards.mdx from provider getting-started pages.
Scans docs/user-guide/providers//getting-started-*.mdx, keeps only the
-providers that Prowler App/Cloud actually supports (source of truth: the
+providers that Prowler Cloud and Prowler Local Server actually support (source of truth: the
`ProviderChoices` enum in api/src/backend/api/models.py — CLI-only providers
such as Linode/LLM/Scaleway/StackIT are excluded), reads the frontmatter
`title`, derives a display name, and emits a snippet exporting a
diff --git a/docs/security/index.mdx b/docs/security/index.mdx
index f9c4a91e0d..e2b004a254 100644
--- a/docs/security/index.mdx
+++ b/docs/security/index.mdx
@@ -28,7 +28,7 @@ All Prowler code goes through the same security pipeline, whether running on Pro
| **Updates** | Automatic | Manual |
-Self-Managed includes Prowler App and Prowler CLI. They can run anywhere — any cloud provider, any region, on-premises, or air-gapped environments. Full control over data residency and infrastructure decisions. See the [Prowler App Installation Guide](/getting-started/installation/prowler-app) to get started.
+Self-Managed includes Prowler Local Server and Prowler CLI. They can run anywhere — any cloud provider, any region, on-premises, or air-gapped environments. Full control over data residency and infrastructure decisions. See the [Prowler Local Server Installation Guide](/getting-started/installation/prowler-app) to get started.
---
diff --git a/docs/security/networking.mdx b/docs/security/networking.mdx
index 0939972ad4..4bcdae818c 100644
--- a/docs/security/networking.mdx
+++ b/docs/security/networking.mdx
@@ -17,6 +17,18 @@ Resolve the egress IP via DNS:
dig egress.prowler.com +short
```
+
+The egress IP address is stable, but it is recommended to periodically verify it remains current by querying `egress.prowler.com`.
+
+
+## Use Cases
+
+Allowlisting Prowler Cloud's egress IP address enables:
+
+- **Credential Usage Control**: Restrict where cloud provider credentials can be used from across AWS, Azure, GCP, and other providers
+- **Kubernetes Security**: Limit inbound HTTPS traffic to clusters by allowing only Prowler Cloud's IP address
+- **Compliance Requirements**: Meet security policies requiring allowlisting of external services
+
## Contact
For questions about networking, visit the [Support page](/support).
diff --git a/docs/security/software-security.mdx b/docs/security/software-security.mdx
index 348503a1e4..0b4cb6a90d 100644
--- a/docs/security/software-security.mdx
+++ b/docs/security/software-security.mdx
@@ -42,7 +42,7 @@ Every GitHub Actions workflow uses runner hardening, pinned action versions, and
### Workflow Security Audit With Zizmor
-- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs via [`ci-zizmor.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ci-zizmor.yml).
+- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs on every pull request and push.
- Triggers on every push, every pull request that touches `.github/`, and on a daily schedule.
- Results upload to the GitHub Security tab via Static Analysis Results Interchange Format (SARIF).
- Key [audit rules](https://docs.zizmor.sh/audits/) the build gates on:
@@ -66,19 +66,19 @@ Multiple SAST tools run on every push and pull request to catch vulnerabilities
### Cross-Language
-- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan, via [`sdk-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-codeql.yml), [`api-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-codeql.yml), and [`ui-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-codeql.yml). Results upload to the GitHub Security tab via SARIF.
+- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan. Results upload to the GitHub Security tab via SARIF.
### Python (SDK + API)
-- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every PR/push in [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml) and [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml).
-- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml) and [`api-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-code-quality.yml).
-- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every PR/push in `sdk-security.yml` and `api-security.yml`.
-- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml).
+- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every pull request and push.
+- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every pull request and push.
+- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every pull request and push.
+- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every pull request and push.
### JavaScript/TypeScript (UI)
-- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every PR/push via `pnpm run healthcheck` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml).
-- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every PR/push via `pnpm run healthcheck` in `ui-tests.yml`.
+- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every pull request and push via `pnpm run healthcheck`.
+- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every pull request and push via `pnpm run healthcheck`.
- **Knip:** dead-code and unused-export detection for the UI. The UI analogue to Vulture.
@@ -95,12 +95,12 @@ Dependencies are scanned against public vulnerability databases on every pull re
### Cross-Language
-- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs via [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml), [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml), and [`ui-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-security.yml).
+- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs on every pull request and push.
- The action installs the `osv-scanner` binary and verifies its SHA-256 checksum against the upstream-signed `SHA256SUMS` manifest before running. Any mismatch aborts the scan.
- Gates the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings.
- Posts and updates a per-lockfile report as a pull request comment.
- Per-vulnerability ignores live in [`osv-scanner.toml`](https://github.com/prowler-cloud/prowler/blob/master/osv-scanner.toml) at the repo root, each with a reason and an expiry date.
-- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
+- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs on every pull request and push that touches an image or its dependencies. Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
- **Dependabot:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) for monthly updates of the SDK Python dependencies, GitHub Actions, Docker base images, and pre-commit hooks. Dependabot opens pull requests for known security advisories, so critical patches reach the team without delay. A 7-day default cooldown reduces exposure to compromised package releases.
- **Renovate:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/renovate.json) dependency update automation is transitioning from Dependabot to **Renovate** to gain finer control over update cadence, grouping, and per-component scope. Both tools currently run in parallel during the migration.
@@ -127,7 +127,7 @@ Dependabot is paused for the API and UI; Renovate now handles those components.
### JavaScript/TypeScript (UI)
-- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
+- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit`. Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
## Supply-Chain Pinning
@@ -151,7 +151,7 @@ The controls applied across all three:
- **uv itself pinned** in the [`setup-python-uv`](https://github.com/prowler-cloud/prowler/tree/master/.github/actions/setup-python-uv) composite action.
-The MCP Server has a small direct-dependency surface and does not yet declare a separate constraint set. Its lock file is the source of truth.
+The MCP Server declares a small constraint set of its own, covering transitive pins that `fastmcp` does not raise on its own. Its lock file remains the source of truth for everything else.
### JavaScript/TypeScript (pnpm)
@@ -159,7 +159,7 @@ The MCP Server has a small direct-dependency surface and does not yet declare a
The UI uses [pnpm](https://pnpm.io) with supply-chain controls configured in [`ui/pnpm-workspace.yaml`](https://github.com/prowler-cloud/prowler/blob/master/ui/pnpm-workspace.yaml).
- **Minimum release age** (`minimumReleaseAge: 1440`): packages must publish at least 24 hours before install. This reduces exposure during the window when a compromised release has not yet been detected and yanked.
-- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `@heroui/shared-utils`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
+- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
- **Trust policy** (`trustPolicy: no-downgrade`): the install fails when a package's trust evidence drops, for example after a new publisher takes over.
- **Block exotic subdeps** (`blockExoticSubdeps: true`): transitive dependencies cannot ship as git URLs or tarballs. Every package in the tree resolves from the configured registry.
- **Transitive overrides** in [`ui/package.json`](https://github.com/prowler-cloud/prowler/blob/master/ui/package.json) force specific versions for transitive packages (`lodash`, `serialize-javascript`, `qs`, `rollup`, `minimatch`, `ajv`, and others).
@@ -182,8 +182,8 @@ Container images get scanned twice: once in CI before they push to a registry, a
### Pre-Publish (CI)
-- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds can fail on critical findings when configured to.
-- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and in the same `*-container-checks.yml` workflows linked above.
+- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs on every pull request and push that touches an image or its dependencies. Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds fail on any critical finding that is not explicitly accepted. Accepted findings live in [`.trivyignore.yaml`](https://github.com/prowler-cloud/prowler/blob/master/.trivyignore.yaml), each carrying a reason and an expiry date, the same policy `osv-scanner.toml` follows. A local `trivy image` run does not apply these suppressions unless you pass `--ignorefile .trivyignore.yaml`: Trivy auto-loads only the classic `.trivyignore` format, never the YAML one.
+- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and alongside the image scans above.
### Post-Publish (Registries)
@@ -191,9 +191,27 @@ Container images get scanned twice: once in CI before they push to a registry, a
- **Docker Hub:** Docker Hub continuously scans the same images mirrored from ECR.
- The security team reviews findings from both registries for triage and remediation.
+### Known Findings
+
+A small number of findings remain in the published images and cannot be resolved by Prowler: the upstream project has released no fix, the package cannot be removed without breaking the image, or the finding comes from a vendored SBOM rather than from a package that is actually installed. Alternative base distributions have been evaluated and none currently satisfies both the vulnerability profile and the runtime requirements of every supported provider.
+
+Each suppression is recorded in [`.trivyignore.yaml`](https://github.com/prowler-cloud/prowler/blob/master/.trivyignore.yaml) with the reason it cannot be fixed, why it is not exploitable in Prowler's runtime, and an expiry date that forces re-review. Nothing is suppressed without that rationale, and a build fails on any critical finding that is not listed there.
+
+To see the current set for any image, scan it directly. This reports everything, including the accepted findings above, because Trivy does not read `.trivyignore.yaml` unless it is named:
+
+```bash
+trivy image prowlercloud/prowler:latest
+```
+
+To see only what is *not* already accepted, point Trivy at the suppression file:
+
+```bash
+trivy image --ignorefile .trivyignore.yaml prowlercloud/prowler:latest
+```
+
## Secrets Detection
-- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request via [`find-secrets.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/find-secrets.yml). Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
+- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request. Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
- A pre-commit hook runs the same check locally and blocks secrets before they leave the developer machine.
## Security Monitoring
diff --git a/docs/snippets/applies-to.mdx b/docs/snippets/applies-to.mdx
new file mode 100644
index 0000000000..0ce22af78d
--- /dev/null
+++ b/docs/snippets/applies-to.mdx
@@ -0,0 +1,14 @@
+export const AppliesTo = ({ products = ["Prowler Cloud", "Prowler Private Cloud", "Prowler Local Server"] }) => {
+ return (
+
+ This guide applies to{" "}
+ {products.map((name, index) => (
+
+ {index > 0 && (index === products.length - 1 ? (products.length > 2 ? ", and " : " and ") : ", ")}
+ {name}
+
+ ))}
+ . See Prowler product families.
+
+ );
+};
diff --git a/docs/snippets/subscription-banner.mdx b/docs/snippets/subscription-banner.mdx
index 8313997c84..90d9b651b5 100644
--- a/docs/snippets/subscription-banner.mdx
+++ b/docs/snippets/subscription-banner.mdx
@@ -1,7 +1,7 @@
-export const SubscriptionBanner = ({ children }) => {
+export const SubscriptionBanner = ({ children, label = "feature" }) => {
return (
- This feature is available exclusively in Prowler Cloud and Prowler Enterprise with a subscription.
+ This {label} is available exclusively in Prowler Cloud and Prowler Private Cloud with a subscription.
{children}
);
diff --git a/docs/style.css b/docs/style.css
index 5c626fb06b..2460a1ae19 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -66,3 +66,62 @@
color: #000000;
border: none;
}
+
+/* Cloud marker: subscription-gated sections and pages (Prowler Cloud / Prowler Private Cloud).
+ Rendered as a trailing ::after glyph so sidebar labels stay left-aligned.
+ Nested groups render as li[data-title] with a button toggle; top-level groups
+ render as h3 headings. Every ungated group that shares a name with a gated one
+ (Providers, Prowler Cloud, Prowler Lighthouse AI) is top-level,
+ so plain li[data-title] selectors match only the gated nested groups, folded
+ or unfolded. The Security tab group is top-level (h3) and always expanded,
+ so it is selected through its sibling list content with :has().
+ Pages are selected by their li id, which equals the page URL. The strict
+ > div > div > span:first-child path targets the title span only, never the
+ nested spans of a tag pill such as Coming Soon.
+ Icon source: /images/icons/cloud-bold.svg. See AGENTS.md "Cloud Marker" convention. */
+li[data-title="Prowler Cloud"] > button span:first-child::after,
+li[data-title="Prowler Lighthouse AI"] > button span:first-child::after,
+li[data-title="Providers"] > button span:first-child::after,
+li[data-title="Scans"] > button span:first-child::after,
+li[data-title="Prowler MCP"] > button span:first-child::after,
+li[data-title="Prowler for AI Agents"] > button span:first-child::after,
+div:has(+ ul a[href="/security/encryption"]) h3 span::after,
+li[id="/user-guide/compliance/tutorials/cross-provider-compliance"] a > div > div > span:first-child::after,
+li[id="/user-guide/compliance/tutorials/cross-provider-type-compliance"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-alerts"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-app-attack-paths-active-queries"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-app-findings-triage"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-app-scan-configuration"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-cloud-aws-organizations"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-cloud-azure-management-groups"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-cloud-gcp-organizations"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-cloud-lighthouse-multi-llm"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-import-findings"] a > div > div > span:first-child::after,
+li[id="/user-guide/tutorials/prowler-scan-scheduling"] a > div > div > span:first-child::after {
+ content: "";
+ display: inline-block;
+ width: 0.875rem;
+ height: 0.875rem;
+ margin-left: 0.375rem;
+ vertical-align: -0.125rem;
+ background: url("/images/icons/cloud-bold.svg") no-repeat center / contain;
+}
+/* Wider sidebar: +2rem over the theme default (18rem) so gated labels with the
+ cloud marker fit on one line. The content column offsets are coupled to the
+ sidebar width and must shift by the same amount, hence the two companion
+ overrides selected by their Tailwind arbitrary-value class substrings. */
+@media (min-width: 1024px) {
+ #sidebar {
+ width: 20rem !important;
+ }
+
+ div[class*="pl-[23.7rem]"] {
+ padding-left: 25.7rem !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ div[class*="(100%-28rem)"] {
+ width: calc(100% - 30rem) !important;
+ }
+}
diff --git a/docs/support.mdx b/docs/support.mdx
index 6999d5efbf..07c9843e12 100644
--- a/docs/support.mdx
+++ b/docs/support.mdx
@@ -3,23 +3,25 @@ title: 'Support'
description: 'Get help with Prowler'
---
-## Lighthouse AI
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
-Lighthouse AI is a Cloud Security Analyst chatbot powered by [Prowler MCP](/getting-started/products/prowler-mcp), your 24/7 virtual cloud security analyst. It can:
+## Prowler Lighthouse AI
+
+Prowler Lighthouse AI is your 24/7 cloud security analyst chatbot, the agentic cloud defender. Powered by [Prowler MCP](/getting-started/products/prowler-mcp), it can:
- **Query your security data**: Findings, compliance status, resources, and remediation guidance
-- **Search Prowler Hub**: Over 1,000 security checks and 70+ compliance frameworks
+- **Search Prowler Hub**: Over 2,000 security checks and 70+ compliance frameworks
- **Access documentation**: Search and retrieve Prowler docs contextually
-Available in Prowler Cloud and Prowler App.
+Available in Prowler Cloud, Prowler Private Cloud, and Prowler Local Server.
-[Learn more about Lighthouse AI](/getting-started/products/prowler-lighthouse-ai)
+[Learn more about Prowler Lighthouse AI](/getting-started/products/prowler-cloud-lighthouse)
## Support Desk
-> Available to **Prowler Cloud** customers.
+
-For Prowler Cloud customers, submit support requests through our support desk. We'll route your request to the right team and respond via email.
+Submit support requests through our support desk. We'll route your request to the right team and respond via email.
Contact our support team
diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx
index 3125d74ef5..9e0c849796 100644
--- a/docs/troubleshooting.mdx
+++ b/docs/troubleshooting.mdx
@@ -4,7 +4,9 @@ title: 'Troubleshooting'
import { VersionBadge } from "/snippets/version-badge.mdx"
-## Running `prowler` I get `[File: utils.py:15] [Module: utils] CRITICAL: path/redacted: OSError[13]`
+## Prowler CLI
+
+### 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.
@@ -16,13 +18,15 @@ This error is also related with a lack of system requirements. To improve perfor
See section [Logging](/user-guide/cli/tutorials/logging) for further information or [contact us](/contact).
-## Common Issues with Docker Compose Installation
+## Prowler Local Server
+
+Common issues with the Docker Compose installation of Prowler Local Server.
### Problem adding AWS Provider using "Connect assuming IAM Role" in Docker
See [GitHub Issue #7745](https://github.com/prowler-cloud/prowler/issues/7745) for more details.
-When running Prowler App via Docker, you may encounter errors such as `Provider not set`, `AWS assume role error - Unable to locate credentials`, or `Provider has no secret` when trying to add an AWS Provider using the "Connect assuming IAM Role" option. This typically happens because the container does not have access to the necessary AWS credentials or profiles.
+When running Prowler Local Server via Docker, you may encounter errors such as `Provider not set`, `AWS assume role error - Unable to locate credentials`, or `Provider has no secret` when trying to add an AWS Provider using the "Connect assuming IAM Role" option. This typically happens because the container does not have access to the necessary AWS credentials or profiles.
**Workaround:**
@@ -53,7 +57,7 @@ AWS_PROFILE=prowler-profile
### Scans Complete but Reports Are Missing or Compliance Data Is Empty (`Too many open files` Error)
-When running Prowler App via Docker Compose, scans may complete successfully but reports are not available for download, compliance data shows as empty, or 404 errors appear when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`.
+When running Prowler Local Server via Docker Compose, scans may complete successfully but reports are not available for download, compliance data shows as empty, or 404 errors appear when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`.
This issue occurs because the default file descriptor limits in Docker containers are too low for Prowler's operations. The default `docker-compose.yml` already includes `ulimits` configuration with `nofile` set to `65536` for the `worker` and `worker-beat` services to prevent this issue.
@@ -87,7 +91,7 @@ docker compose up -d
-When Prowler App runs self-hosted on a machine or Kubernetes node with many CPUs,
+When Prowler Local Server runs 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
@@ -196,7 +200,7 @@ A fix addressing this permission issue is being evaluated in [PR #9953](https://
### Scan Stuck in Executing State After Worker Crash
-When running Prowler App via Docker Compose, a scan may remain indefinitely in the `executing` state if the worker process crashes (for example, due to an Out of Memory condition) before it can update the scan status. Since it is not currently possible to cancel a scan in `executing` state through the UI, the workaround is to manually update the scan record in the database.
+When running Prowler Local Server via Docker Compose, a scan may remain indefinitely in the `executing` state if the worker process crashes (for example, due to an Out of Memory condition) before it can update the scan status. Since it is not currently possible to cancel a scan in `executing` state through the UI, the workaround is to manually update the scan record in the database.
**Root Cause:**
diff --git a/docs/user-guide/ai-agents/claude-code.mdx b/docs/user-guide/ai-agents/claude-code.mdx
new file mode 100644
index 0000000000..84763bf173
--- /dev/null
+++ b/docs/user-guide/ai-agents/claude-code.mdx
@@ -0,0 +1,294 @@
+---
+title: "Connect Claude Code to Prowler MCP Server"
+sidebarTitle: "Claude Code"
+---
+
+Connect [Claude Code](https://www.claude.com/product/claude-code) to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp`.
+
+## Where Claude Code Runs
+
+Claude Code runs in two places. Both read the same configuration file, so you set it up **once from a terminal** and it works in both.
+
+| Surface | How you open it | Reads | Covered by |
+|---|---|---|---|
+| **Claude Code CLI** | `claude` in a terminal | `~/.claude.json` | This guide |
+| **Claude Code in the desktop app** | The **Code** tab inside the Claude app | `~/.claude.json` — the same file | This guide, [set up from a terminal](#claude-code-in-the-desktop-app-code-tab) |
+| **Claude app Chat** | The **Chat** tab inside the Claude app | `claude_desktop_config.json` | [Claude App Chat](/user-guide/ai-agents/claude-desktop) — a separate setup |
+
+
+**The Chat tab is not Claude Code.** It is a different product surface with its own configuration file and its own connection method (a local bridge). Nothing on this page applies to it. If you want Prowler in Chat, use the [Claude App Chat](/user-guide/ai-agents/claude-desktop) guide instead.
+
+
+## Choose Your Setup
+
+There are two ways to connect. Both end with the same MCP Server connection, the difference is what comes with it.
+
+| | 🔌 **Prowler Plugin** | ⚙️ **MCP Connection Only** |
+|---|---|---|
+| **What you get** | The MCP connection **plus** the official Prowler skills for cloud security tasks | The MCP connection |
+| **Setup** | Two slash commands, prompts for the API key | One `claude mcp add` command |
+| **Guided workflows** | ✅ Skills drive multi-step security work end to end | ❌ You drive the conversation |
+| **Best for** | Structured cloud security work, such as taking an account to compliance | Ad-hoc queries and your own workflows |
+| **Where to use it** | Claude Code CLI | Claude Code CLI, and the **recommended setup for the desktop app's [Code tab](#claude-code-in-the-desktop-app-code-tab)** |
+
+
+**The plugin already includes the MCP connection.** If you install the plugin, do **not** also run `claude mcp add` — you would end up with the server configured twice.
+
+
+## Prerequisites
+
+- **Claude Code** installed and signed in. See the [official install guide](https://www.claude.com/product/claude-code).
+- **A Prowler Cloud account.** The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
+
+## Get Your Prowler API Key
+
+Create an API key in Prowler Cloud and copy it. The key begins with `pk_` and is shown only once. Check the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide for details.
+
+---
+
+# Option 1: Install the Prowler Plugin
+
+
+**Preview**: this plugin is under active development. Please report issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join the [Slack community](https://goto.prowler.com/slack) for feedback.
+
+
+End-to-end cloud security from inside Claude Code, powered by the Prowler MCP server. The plugin bundles the official Prowler skills, task-specific workflows that let Claude carry out multi-step security work against a Prowler Cloud-connected account, rather than answering one question at a time.
+
+### Included Skills
+
+| Skill | What it does |
+| --- | --- |
+| `prowler:framework-compliance-triage` | Walks an account through a compliance assessment and remediates findings until the chosen security or industry framework is compliant. |
+
+
+More skills are on the way. Installing the plugin keeps you current — new skills arrive with plugin updates, no extra configuration required.
+
+
+## Installation (Claude Code CLI)
+
+
+
+ Inside a Claude Code session:
+
+ ```text
+ /plugin marketplace add prowler-cloud/prowler
+ /plugin install prowler@prowler-plugins
+ ```
+
+
+ If you already have the repository checked out:
+
+ ```text
+ /plugin marketplace add /absolute/path/to/prowler
+ /plugin install prowler@prowler-plugins
+ ```
+
+
+
+On first install, Claude Code prompts for your **Prowler API key**. The value is stored securely (macOS keychain or `~/.claude/.credentials.json`) and used to authenticate against Prowler Cloud.
+
+## Verify the Installation
+
+In a Claude Code session:
+
+```text
+/mcp → "prowler" appears as a connected server
+/plugin → "prowler" enabled, with the bundled Prowler skills listed
+```
+
+If `/mcp` reports the `prowler` server as failed, the most common cause is a rejected API key, re-issue one in Prowler Cloud and reinstall the plugin so it re-prompts.
+
+## Usage
+
+Describe the security task you want done and Claude selects the matching skill.
+
+### Framework Compliance Triage
+
+Mention the framework you want to comply with:
+
+- *"Make my AWS production account compliant with CIS 4.0."*
+- *"Make my current Terraform project compliant with Prowler ThreatScore Compliance Framework based on the latest scan results."*
+- *"Help me get to 100% on PCI-DSS for this GCP project."*
+
+You pick a **primary tool** (Terraform, gh / az / aws CLI, web console, or mixed) and a **mode**:
+
+
+
+ Claude shows each fix — target resource, exact commands, side effects, reversibility — and waits for your go-ahead before applying.
+
+
+ Claude presents a single up-front plan grouped by shared fixes, waits for one confirmation, then proceeds. It pauses mid-loop if a fix has wide blast radius or a finding is not applicable.
+
+
+
+Claude tracks progress in a markdown report under `.prowler/` at your project root — one file per framework × account. Open it any time to see exactly where the flow is. When all findings are addressed, Claude proposes a fresh Prowler scan to verify everything end-to-end.
+
+## Uninstalling
+
+```text
+/plugin uninstall prowler@prowler-plugins
+/plugin marketplace remove prowler-plugins
+```
+
+The stored API key is removed automatically.
+
+---
+
+# Option 2: Connect the MCP Server Only
+
+Choose this when you want Prowler's tools available without the Prowler skills.
+
+## Add the Server
+
+Claude Code connects to remote HTTP MCP servers natively and supports custom headers, so no bridge is required.
+
+```bash
+export PROWLER_API_KEY="pk_your_api_key_here"
+
+claude mcp add --transport http prowler https://mcp.prowler.com/mcp \
+ --header "Authorization: Bearer $PROWLER_API_KEY" \
+ --scope user
+```
+
+
+
+
+
+
+**Always pass `--scope user`.** The default scope is `local`, which binds the server to the single directory you ran the command in. A locally-scoped server does not load when you open Claude Code anywhere else — this is the most common reason Prowler tools appear to vanish.
+
+
+| Scope | Loads in | Shared | Stored in |
+|-------|----------|--------|-----------|
+| `user` | All your projects | No | `~/.claude.json`, top-level `mcpServers` |
+| `project` | Current project only | Yes, via version control | `.mcp.json` in the project root |
+| `local` (default) | Current project only | No | `~/.claude.json`, under that project's entry |
+
+When the same server name exists in more than one scope, precedence is **local → project → user**. The winning entry is used whole; fields are not merged.
+
+
+Avoid `--scope project` for Prowler. That writes `.mcp.json` into your repository, and committing the file would publish your API key.
+
+
+
+**Local server:** Replace the URL with your own HTTP endpoint. Everything else stays the same.
+
+
+## Verify the Connection
+
+```bash
+claude mcp get prowler # shows which scope holds the definition
+claude mcp list # lists all servers and their status
+```
+
+Inside a Claude Code session, run `/mcp` to see connected servers and their tools.
+
+
+
+
+
+## Start Using Prowler MCP
+
+- *"Show me all critical findings from my AWS accounts"*
+- *"What does the S3 bucket public access check do?"*
+- *"Onboard this new AWS account in my Prowler organization"*
+
+
+
+
+
+---
+
+# Claude Code in the Desktop App (Code Tab)
+
+The **Code** tab in the Claude desktop app runs the same Claude Code as the CLI, and reads the same `~/.claude.json`. There is no separate Prowler setup for it — you configure it **from a terminal** and the Code tab picks it up.
+
+
+**Use [Option 2](#option-2-connect-the-mcp-server-only) with `--scope user` here.** It is the recommended setup for the Code tab. The Prowler plugin ([Option 1](#option-1-install-the-prowler-plugin)) is not the recommended route for the desktop app — install it in the Claude Code CLI instead.
+
+
+
+**You cannot do this from inside the app.** The desktop app has no interface for adding an MCP server to a Claude Code session. **Settings → Connectors** configures the **Chat** tab, not the **Code** tab, so anything added there never reaches Claude Code. Trying to configure it from the app is the main reason this appears not to work.
+
+
+
+
+ In a normal terminal — not inside the app:
+
+ ```bash
+ export PROWLER_API_KEY="pk_your_api_key_here"
+
+ claude mcp add --transport http prowler https://mcp.prowler.com/mcp \
+ --header "Authorization: Bearer $PROWLER_API_KEY" \
+ --scope user
+ ```
+
+ `--scope user` is what makes this work. It writes to `~/.claude.json`, the file the Code tab reads.
+
+
+
+ ```bash
+ claude mcp get prowler
+ ```
+
+ The scope must be `user`. A `local`-scoped server is bound to the directory you ran the command in and will not load in an app session opened elsewhere.
+
+
+
+ Quit the app completely and reopen it. Configuration is read at startup.
+
+
+
+ Open a **Code** tab session and ask for a Prowler tool: "Do you have access to the Prowler MCP tools?", it should respond with a list of available tools or confirming that it has access.
+
+
+
+---
+
+# Claude App Chat (Chat Tab)
+
+Not covered by this page. The **Chat** tab is a separate surface: it does not read `~/.claude.json`, so a server added with `claude mcp add` appears in the CLI and in the Code tab but **never** in Chat. That is expected behavior, not a broken setup.
+
+Chat reads `claude_desktop_config.json` and reaches the Prowler MCP Server through a local bridge.
+
+
+ Separate guide: local bridge and its own configuration file
+
+
+---
+
+# Troubleshooting
+
+| Symptom | Likely cause | Fix |
+| --- | --- | --- |
+| `/mcp` shows `prowler` as failed | Rejected API key | Generate a new one in Prowler Cloud. With the plugin, reinstall it to re-prompt. |
+| No MCP servers configured | Server added at `local` scope from another directory | Run `claude mcp get prowler`, then re-add with `--scope user`. |
+| A stale entry overrides a working one | Precedence is local → project → user | `claude mcp remove prowler --scope local` |
+| Tools appear in the CLI but not in the app's **Code** tab | Server added at `local` scope, or the app was not restarted | Re-add with `--scope user`, then quit and reopen the app. See [Claude Code in the Desktop App](#claude-code-in-the-desktop-app-code-tab). |
+| Tools appear in the **Code** tab but not the **Chat** tab | Chat is a different surface with its own config file | Expected. Set Chat up separately, see [Claude App Chat](/user-guide/ai-agents/claude-desktop). |
+| No way to add the server from inside the app | The app has no MCP interface for Claude Code sessions | Configure it from a terminal with `--scope user`, then restart the app. See [Claude Code in the Desktop App](#claude-code-in-the-desktop-app-code-tab). |
+| Skill not invoked when expected | The prompt didn't match any skill's description | Name the task explicitly. For compliance triage, mention the framework plus "compliance" or "compliant". |
+| "Framework not supported" | Prowler Hub does not list the framework for that provider | Open an issue or PR at [github.com/prowler-cloud/prowler](https://github.com/prowler-cloud/prowler). |
+
+### Authentication Fails With 401
+
+- Confirm the header value includes the `Bearer ` prefix.
+- Check that `PROWLER_API_KEY` was set when you ran `claude mcp add` — the shell expands it at that moment and stores the resulting literal value. If the variable was empty, the stored header reads `Bearer ` with nothing after it. Verify with `claude mcp get prowler`.
+- Confirm the key has not been revoked in Prowler Cloud.
+
+## Next Steps
+
+
+
+ Explore all available tools and capabilities
+
+
+ Configuration reference for every supported client
+
+
+
+## Getting Help
+
+- Search for existing [GitHub issues](https://github.com/prowler-cloud/prowler/issues)
+- Ask for help in our [Slack community](https://goto.prowler.com/slack)
+- Report a new issue on [GitHub](https://github.com/prowler-cloud/prowler/issues/new)
diff --git a/docs/user-guide/ai-agents/claude-desktop.mdx b/docs/user-guide/ai-agents/claude-desktop.mdx
new file mode 100644
index 0000000000..1a09c43116
--- /dev/null
+++ b/docs/user-guide/ai-agents/claude-desktop.mdx
@@ -0,0 +1,142 @@
+---
+title: "Connect the Claude App Chat to Prowler MCP Server"
+sidebarTitle: "Claude App (Chat)"
+---
+
+Connect the **Chat** tab of the Claude desktop app to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp`.
+
+
+**This page covers the Chat tab only.** Looking for **Claude Code** — either the CLI or the app's **Code** tab? Those are a different surface, with a different configuration file and a different connection method. See [Connect Claude Code](/user-guide/ai-agents/claude-code).
+
+
+## Prerequisites
+
+- **Claude desktop app** installed and signed in.
+- **Node.js and npm**, to install the bridge.
+- **A Prowler Cloud account.** The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
+
+## Why "Add Custom Connector" Does Not Work
+
+The app's **Settings → Connectors → Add custom connector** dialog is the obvious place to paste an MCP URL, but it does not fit the Prowler Cloud MCP Server for two independent reasons:
+
+1. **Connectors authenticate with OAuth.** Authenticating with a fixed API key sent as a request header is a separate mechanism that Anthropic documents as **beta**, rolled out on request. Without it, the dialog offers a URL and OAuth client credentials, with nowhere to supply `Authorization: Bearer pk_...`.
+2. **Connectors do not connect from your machine.** Claude reaches your MCP server from Anthropic's cloud infrastructure rather than your local device. A Prowler MCP Server on `localhost`, behind a VPN, or restricted by an IP allowlist is unreachable that way regardless of authentication.
+
+Use a local bridge instead, as described below.
+
+## Step 1: Get Your Prowler API Key
+
+Create an API key in Prowler Cloud and copy it. The key begins with `pk_` and is shown only once. Check the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide for details.
+
+## Step 2: Install the Bridge
+
+`mcp-remote` presents the remote HTTP server to Claude as a local STDIO server and injects the `Authorization` header. Install a pinned version into a dedicated directory:
+
+```bash
+mkdir -p ~/.local/share/prowler-mcp-bridge
+cd ~/.local/share/prowler-mcp-bridge
+npm init -y
+npm install --save-exact mcp-remote@0.1.38
+```
+
+
+Do not configure Claude to run `npx mcp-remote` directly. `npx` can fetch and execute a new version on every launch, which means unreviewed code runs with access to your API key. Install a pinned version and point Claude at the installed binary.
+
+
+
+`mcp-remote` is community-maintained and is not an Anthropic product. Review it before use.
+
+
+## Step 3: Edit the Configuration File
+
+In the Claude app, go to **Settings → Developer** and click **Edit Config**. This reveals `claude_desktop_config.json`:
+
+- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
+- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
+
+
+
+
+
+Add the following, replacing the `command` path with the absolute path to the installed binary and the placeholder with your API key:
+
+```json
+{
+ "mcpServers": {
+ "prowler": {
+ "command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote",
+ "args": [
+ "https://mcp.prowler.com/mcp",
+ "--header",
+ "Authorization: Bearer ${PROWLER_API_KEY}"
+ ],
+ "env": {
+ "PROWLER_API_KEY": "pk_your_api_key_here"
+ }
+ }
+ }
+}
+```
+
+
+**Local server:** Replace the URL with your own HTTP endpoint. Everything else stays the same.
+
+
+## Step 4: Restart the App
+
+Quit the Claude app completely and reopen it. Configuration is read at startup.
+
+## Step 5: Start Using Prowler MCP
+
+Open a Chat conversation and ask questions that use the Prowler tools:
+
+- *"Show me all critical findings from my AWS accounts"*
+- *"What does the S3 bucket public access check do?"*
+- *"Summarize my CIS compliance status by provider"*
+
+
+
+
+
+## Troubleshooting
+
+### Server Does Not Appear After Editing the Config
+
+- Quit and reopen the app entirely — closing the window is not enough on macOS.
+- Confirm `claude_desktop_config.json` is valid JSON.
+- Confirm the `command` path points at a real executable. A wrong path surfaces as the server failing to start rather than as an auth error.
+
+### Tools Appear in Claude Code but Not in Chat
+
+Expected. The Chat tab does not read `~/.claude.json`, so servers added with `claude mcp add` never appear here. The Chat tab needs an entry in `claude_desktop_config.json`, which is what this guide sets up.
+
+### Authentication Fails With 401
+
+- Confirm the header value includes the `Bearer ` prefix.
+- Confirm the key has not been revoked in Prowler Cloud.
+
+### Checking the Logs
+
+- **macOS:** `~/Library/Logs/Claude/mcp*.log`
+- **Windows:** `%APPDATA%\Claude\logs\mcp*.log`
+
+```bash
+tail -f ~/Library/Logs/Claude/mcp*.log
+```
+
+## Next Steps
+
+
+
+ Explore all available tools and capabilities
+
+
+ Configuration reference for every supported client
+
+
+
+## Getting Help
+
+- Search for existing [GitHub issues](https://github.com/prowler-cloud/prowler/issues)
+- Ask for help in our [Slack community](https://goto.prowler.com/slack)
+- Report a new issue on [GitHub](https://github.com/prowler-cloud/prowler/issues/new)
diff --git a/docs/user-guide/ai-agents/codex.mdx b/docs/user-guide/ai-agents/codex.mdx
new file mode 100644
index 0000000000..cd72b7781a
--- /dev/null
+++ b/docs/user-guide/ai-agents/codex.mdx
@@ -0,0 +1,188 @@
+---
+title: "Connect Codex / ChatGPT Desktop to Prowler MCP Server"
+sidebarTitle: "Codex / ChatGPT"
+---
+
+Connect [OpenAI Codex](https://learn.chatgpt.com/docs/extend/mcp) to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp` so Codex can query findings, inspect checks, and manage your Prowler providers.
+
+## Which Codex Surfaces Work
+
+Codex keeps MCP servers in one file, `~/.codex/config.toml`. You can set it up from either the **Codex / ChatGPT desktop app** or the **Codex CLI** — both write to that same file, so pick whichever you already use.
+
+| Surface | Set it up here | Notes |
+|---------|----------------|-------|
+| **[Codex / ChatGPT desktop app](https://learn.chatgpt.com/docs/app)** (macOS, Windows) | ✅ Yes | **Settings → MCP servers** |
+| **Codex CLI** (terminal) | ✅ Yes | `codex mcp` commands |
+| **Codex IDE extension** (VS Code) | Inherits | Works automatically once the app or CLI is configured |
+| **ChatGPT on the web** | ❌ No | Does not read local Codex configuration |
+
+
+**Codex and ChatGPT share one desktop app.** Since July 2026 the standalone Codex app and the ChatGPT desktop app are the same application: Codex is a dedicated coding surface inside it, alongside Chat and Work. If you already had the Codex app, updating turns it into the new ChatGPT desktop app and it still opens in Codex. Either way, this guide applies.
+
+Not to be confused with **ChatGPT Classic**, the name given to the previous-generation ChatGPT desktop app.
+
+
+
+**Configure once, use everywhere.** The Codex documentation states that the ChatGPT desktop app, Codex CLI, and IDE extension "share this configuration. Once you configure your MCP servers, you can switch among those clients without redoing setup." Set the server up in the app or the CLI and the IDE extension picks it up with no extra work.
+
+
+## Prerequisites
+
+- **The Codex / ChatGPT desktop app, or Codex CLI 0.46.0 or later.** Remote MCP servers over streamable HTTP were added to the CLI in 0.46.0 — check with `codex --version` and upgrade if needed.
+- **A Prowler Cloud account.** The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
+
+## Step 1: Get Your Prowler API Key
+
+Create an API key in Prowler Cloud and copy it. The key begins with `pk_` and is shown only once. Check the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide for details.
+
+## Step 2: Add the Prowler MCP Server
+
+The Prowler MCP Server needs two request headers: `Authorization` to authenticate you, and `User-Agent` because Codex does not send one by default.
+
+Each tab below is a complete setup — follow the one that matches the surface you use.
+
+
+
+ 1. Open **Settings** and select **Plugins → MCPs**
+ 2. Click **Add server**
+ 3. Enter `prowler` as the name and choose type **Streamable HTTP**
+ 4. Enter the URL `https://mcp.prowler.com/mcp`
+ 5. Add two headers:
+
+ | Header | Value |
+ |--------|-------|
+ | `Authorization` | `Bearer pk_your_api_key_here` |
+ | `User-Agent` | `codex` |
+
+ 6. Save the server
+
+
+
+
+
+
+ **Enter the key directly here rather than using an environment variable.** Codex can read credentials from an environment variable, but desktop applications do not reliably inherit variables exported in a shell profile — on macOS an app launched from Finder or the Dock typically sees none of them. Pasting the key into the dialog is the approach that works consistently in the app.
+
+
+
+ **This stores your API key in plain text** in `~/.codex/config.toml`. Treat that file accordingly: exclude it from dotfile repositories and config sync, and create the key from an account with the minimum permissions you need so its exposure is limited. Revoke and re-issue the key in Prowler Cloud if the file is ever shared.
+
+
+
+
+ Register the server:
+
+ ```bash
+ codex mcp add prowler --url https://mcp.prowler.com/mcp
+ ```
+
+ Codex confirms with `Added global MCP server 'prowler'.`
+
+ Then add both headers by hand, since `codex mcp add` has no flag for headers. Open `~/.codex/config.toml` and complete the entry:
+
+ ```toml
+ [mcp_servers.prowler]
+ url = "https://mcp.prowler.com/mcp"
+ http_headers = { Authorization = "Bearer pk_your_api_key_here", "User-Agent" = "codex" }
+ ```
+
+
+ **Write the key literally rather than using an environment variable.** This is the form that works across every Codex surface. All of them read this same file, but only the CLI reliably sees variables exported in your shell profile — see the warning below.
+
+
+
+ **This stores your API key in plain text** in `~/.codex/config.toml`. Treat that file accordingly: exclude it from dotfile repositories and config sync, and create the key from an account with the minimum permissions you need so its exposure is limited. Revoke and re-issue the key in Prowler Cloud if the file is ever shared.
+
+
+
+
+Restart Codex once you are done.
+
+
+**Local server:** Replace the URL with your own HTTP endpoint. Everything else stays the same.
+
+
+## Step 3: Verify the Connection
+
+Run `/mcp` in the app or in a CLI session to list connected servers and their tools.
+
+
+
+
+
+From the CLI you can also inspect the stored entry directly:
+
+```bash
+codex mcp list # one row per server, with status and auth
+codex mcp get prowler # full entry, header values masked
+```
+
+
+**Verify rather than assume.** Codex silently ignores unrecognized keys in `config.toml` — a misspelled key name produces no error at all, and the server simply never receives your credentials. Always confirm with `codex mcp get prowler` after editing the file by hand.
+
+
+## Step 4: Start Using Prowler MCP
+
+Ask Codex questions that use the Prowler tools:
+
+- *"Show me all critical findings from my AWS accounts"*
+- *"What does the S3 bucket public access check do?"*
+- *"List my connected Prowler providers and their last scan date"*
+
+
+
+
+
+## Troubleshooting
+
+### Startup Fails With HTTP 403 Forbidden
+
+Codex reports a handshake failure on startup, with an HTML error page rather than a JSON response:
+
+```
+⚠ MCP client for `prowler` failed to start: MCP startup failed: handshaking with MCP server
+ failed: ... unexpected server response: HTTP 403:
+ 403 Forbidden
+```
+
+The `User-Agent` header is missing. Codex's HTTP client does not send one, and requests without it are rejected before reaching the MCP server. Note this is a **403**, not a 401 — so it is not an API key problem. Add the header as shown in [Step 2](#step-2-add-the-prowler-mcp-server); the value itself does not matter, only that the header is present.
+
+### Authentication Fails With 401
+
+- Run `codex mcp get prowler` and confirm the entry has the headers you expect. Values are masked, but a missing header shows as `-`.
+- If you used a literal header, confirm the value starts with `Bearer ` and contains the full key.
+- **If it works in the CLI but fails in the desktop app or the VS Code extension, you are almost certainly using an environment variable.** Those surfaces do not inherit your shell profile. Switch that entry to a literal `Authorization` header as shown in [Step 2](#step-2-add-the-prowler-mcp-server).
+- If you use an environment variable, verify it is set in the environment Codex was launched from: `echo $PROWLER_API_KEY`.
+- With `env_http_headers` the variable must include the `Bearer ` prefix. With `bearer_token_env_var` it must **not** — Codex adds the prefix itself.
+- Confirm the key has not been revoked in Prowler Cloud.
+
+### Server Not Listed
+
+- Confirm your Codex CLI version is 0.46.0 or later with `codex --version`.
+- Run `codex mcp get prowler`. If it reports the server is not found, the entry was not written or the TOML table name is misspelled.
+- Check for a typo in the key names. Codex ignores unknown keys without warning.
+
+### Project-Scoped Config Is Ignored
+
+A `.codex/config.toml` inside a project is loaded **only when the project is trusted**. If your entry lives there and does nothing, trust the project or move the entry to `~/.codex/config.toml`.
+
+### Tools Do Not Appear After Editing the Config
+
+Restart Codex. Configuration is read at startup. In the app, quit completely and reopen it, sometimes just closing the window is not enough.
+
+## Next Steps
+
+
+
+ Explore all available tools and capabilities
+
+
+ Configuration reference for every supported client
+
+
+
+## Getting Help
+
+- Search for existing [GitHub issues](https://github.com/prowler-cloud/prowler/issues)
+- Ask for help in our [Slack community](https://goto.prowler.com/slack)
+- Report a new issue on [GitHub](https://github.com/prowler-cloud/prowler/issues/new)
diff --git a/docs/user-guide/ai-agents/cursor.mdx b/docs/user-guide/ai-agents/cursor.mdx
new file mode 100644
index 0000000000..5e28eeeb1f
--- /dev/null
+++ b/docs/user-guide/ai-agents/cursor.mdx
@@ -0,0 +1,171 @@
+---
+title: "Connect Cursor to Prowler MCP Server"
+sidebarTitle: "Cursor"
+---
+
+Connect [Cursor](https://cursor.com/docs/mcp) to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp` so the Cursor agent can query findings, inspect security checks, and manage your Prowler providers while you work.
+
+Cursor supports remote MCP servers over HTTP natively, so no bridge or local installation is required.
+
+## Prerequisites
+
+- **Cursor** installed and authenticated. See the [official install guide](https://cursor.com/download).
+- **A Prowler Cloud account.** The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
+
+## Step 1: Get Your Prowler API Key
+
+Create an API key in Prowler Cloud and copy it. The key begins with `pk_` and is shown only once. Check the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide for details.
+
+## Step 2: Add the Prowler MCP Server
+
+Cursor reads MCP servers from an `mcp.json` file. Choose the scope that fits your use case:
+
+| Scope | File | Applies to |
+|-------|------|------------|
+| **Global** | `~/.cursor/mcp.json` | Every project you open in Cursor |
+| **Project** | `.cursor/mcp.json` in the project root | That project only |
+
+Both files are merged. If the same server name appears in both, the project-level entry takes priority.
+
+For Prowler, the **global** scope is usually the right choice — your findings are not tied to a single repository, and it keeps the API key out of any project directory that might be committed.
+
+
+
+ From Agent Window open **Customize** in the Cursor sidebar, then select the MCP section.
+
+ On earlier versions, press `Cmd + Shift + J` (macOS) or `Ctrl + Shift + J` (Windows/Linux) to open Cursor Settings, then click **Tools & MCP** in the sidebar.
+
+
+
+
+
+ Click **New MCP Server** (or **Add Custom MCP**). Cursor opens `mcp.json` in the editor.
+
+
+
+
+
+
+
+ Paste the following, replacing the placeholder with your API key:
+
+ ```json
+ {
+ "mcpServers": {
+ "prowler": {
+ "url": "https://mcp.prowler.com/mcp",
+ "headers": {
+ "Authorization": "Bearer "
+ }
+ }
+ }
+ }
+ ```
+
+ Save the file. Cursor picks up the change and connects to the server.
+
+
+
+
+
+
+
+
+**Local server:** Replace the URL with your own HTTP endpoint. Everything else stays the same.
+
+
+### Keeping the API Key Out of the File
+
+Cursor resolves variables in the `command`, `args`, `env`, `url`, and `headers` fields, so you can reference an environment variable instead of writing the key into `mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "prowler": {
+ "url": "https://mcp.prowler.com/mcp",
+ "headers": {
+ "Authorization": "Bearer ${env:PROWLER_API_KEY}"
+ }
+ }
+ }
+}
+```
+
+Export the variable in your shell profile (`~/.zshrc`, `~/.bashrc`, or equivalent):
+
+```bash
+export PROWLER_API_KEY="pk_your_api_key_here"
+```
+
+
+The syntax is `${env:NAME}`, not a bare `${NAME}`. Restart Cursor after changing your shell profile so it inherits the new value.
+
+
+
+The `envFile` option does **not** work for remote servers — it is STDIO-only. Use `${env:...}` interpolation with variables set in your shell profile instead.
+
+
+This form is strongly recommended when using a **project-scoped** `.cursor/mcp.json`, since that file may be committed to version control.
+
+## Step 3: Verify the Connection
+
+Return to the MCP settings. The `prowler` server should be listed as enabled, with the Prowler tools shown beneath it.
+
+
+
+
+
+## Step 4: Start Using Prowler MCP
+
+Open the chat panel and ask questions that use the Prowler tools:
+
+- *"Show me all critical findings from my AWS accounts"*
+- *"What does the S3 bucket public access check do?"*
+- *"Which of my providers failed the most CIS checks in the last scan?"*
+
+Cursor asks for approval before running an MCP tool the first time.
+
+
+
+
+
+You can toggle individual tools on or off from the tools list at the top of the chat panel, which is useful for keeping the active tool count down.
+
+## Troubleshooting
+
+### Server Does Not Connect
+
+- Check that `mcp.json` is valid JSON. A trailing comma or missing brace prevents the whole file from loading.
+- Open **MCP Logs** in the Output panel for the specific error.
+- Confirm the URL is exactly `https://mcp.prowler.com/mcp`.
+
+### Authentication Fails With 401
+
+- Verify the header value includes the `Bearer ` prefix: `"Bearer pk_..."`, not just the key.
+- Confirm the key has not been revoked in Prowler Cloud.
+- If using `${env:PROWLER_API_KEY}`, check the variable is set in the environment Cursor inherits. Restart Cursor after editing your shell profile — a value exported only in an already-open terminal will not reach the app.
+
+### The Entire `mcp.json` Is Ignored
+
+Remove any `"type": "streamable-http"` field. One such entry causes the Cursor CLI to drop every server in the file silently.
+
+### Some Prowler Tools Are Missing
+
+Cursor limits how many tools it exposes to the agent at once. With several MCP servers enabled you may exceed it, and some tools become unavailable. Disable servers you are not using, or turn off individual tools from the chat panel's tools list.
+
+## Next Steps
+
+
+
+ Explore all available tools and capabilities
+
+
+ Configuration reference for every supported client
+
+
+
+## Getting Help
+
+- Search for existing [GitHub issues](https://github.com/prowler-cloud/prowler/issues)
+- Ask for help in our [Slack community](https://goto.prowler.com/slack)
+- Report a new issue on [GitHub](https://github.com/prowler-cloud/prowler/issues/new)
diff --git a/docs/user-guide/ai-agents/index.mdx b/docs/user-guide/ai-agents/index.mdx
new file mode 100644
index 0000000000..ca05419050
--- /dev/null
+++ b/docs/user-guide/ai-agents/index.mdx
@@ -0,0 +1,49 @@
+---
+title: "Connect Your AI Agent to Prowler"
+sidebarTitle: "Overview"
+description: "Pick your AI agent and follow its guide to connect it to the Prowler Cloud MCP Server."
+---
+
+Connect your AI agent to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp` so it can query findings, inspect security checks, and manage your Prowler providers.
+
+Pick your agent below. Each guide is a full walkthrough with screenshots, verification steps, and the caveats specific to that client.
+
+
+
+ Plugin and MCP-only choices, and which Claude surfaces work
+
+
+ The Chat tab, via a local bridge
+
+
+ ChatGPT Desktop App, Codex CLI, the VS Code extension through same config file
+
+
+ Agentic code editor. Global and project scopes
+
+
+ Agent mode with secure key prompts
+
+
+
+## Before You Start
+
+All guides need the same two things:
+
+- A **Prowler Cloud account** with at least one cloud provider connected. [Sign up](https://cloud.prowler.com) if you do not have one.
+- A **Prowler API key**, created in Prowler Cloud. The key begins with `pk_` and is shown only once. See the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide.
+
+
+Using an agent that is not listed here? Any MCP-compatible client can connect. See the [generic configuration reference](/getting-started/basic-usage/prowler-mcp#cloud-mcp-server-configuration-recommended) for the raw connection details.
+
+
+## Next Steps
+
+
+
+ How the MCP Server fits into Prowler
+
+
+ Cloud and local server options, all clients
+
+
diff --git a/docs/user-guide/ai-agents/vscode.mdx b/docs/user-guide/ai-agents/vscode.mdx
new file mode 100644
index 0000000000..90f41e5816
--- /dev/null
+++ b/docs/user-guide/ai-agents/vscode.mdx
@@ -0,0 +1,145 @@
+---
+title: "Connect VS Code and GitHub Copilot to Prowler MCP Server"
+sidebarTitle: "VS Code / Copilot"
+---
+
+Connect [Visual Studio Code](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) and GitHub Copilot agent mode to the Prowler Cloud MCP Server at `https://mcp.prowler.com/mcp` so Copilot can query findings, inspect security checks, and manage your Prowler providers.
+
+## Prerequisites
+
+- **VS Code 1.102 or later.** MCP support became generally available in 1.102.
+- **GitHub Copilot** enabled, with access to agent mode.
+- **A Prowler Cloud account.** The free tier is enough to start. Sign up at [cloud.prowler.com](https://cloud.prowler.com).
+
+## Step 1: Get Your Prowler API Key
+
+Create an API key in Prowler Cloud and copy it. The key begins with `pk_` and is shown only once. Check the [API Keys](/user-guide/tutorials/prowler-app-api-keys#creating-api-keys) guide for details.
+
+## Step 2: Add the Prowler MCP Server
+
+VS Code stores MCP servers in an `mcp.json` file. Choose the scope that fits your use case:
+
+| Scope | How to open it | Applies to |
+|-------|----------------|------------|
+| **User** | Command palette → **MCP: Open User Configuration** | Every workspace |
+| **Workspace** | `.vscode/mcp.json` in the project root | That workspace only |
+
+For Prowler, the **user** scope is usually the right choice — your findings are not tied to a single repository, and it keeps the API key out of any project directory that might be committed.
+
+
+
+ Open the command palette with `Cmd + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux), then run **MCP: Open User Configuration**.
+
+ VS Code opens your user-level `mcp.json`. Use this command rather than navigating to the file by hand — the file lives inside your active profile folder, and the path differs per profile.
+
+
+
+
+
+
+
+ Paste the following. This version prompts you for the API key on first use and stores it securely, so the key is never written into the file:
+
+ ```json
+ {
+ "inputs": [
+ {
+ "type": "promptString",
+ "id": "prowler-api-key",
+ "description": "Prowler API Key",
+ "password": true
+ }
+ ],
+ "servers": {
+ "prowler": {
+ "type": "http",
+ "url": "https://mcp.prowler.com/mcp",
+ "headers": {
+ "Authorization": "Bearer ${input:prowler-api-key}"
+ }
+ }
+ }
+ }
+ ```
+
+ Save the file.
+
+
+
+
+
+
+
+ Start the server. VS Code prompts for the Prowler API key. Paste it and press Enter — VS Code stores it securely and does not ask again.
+
+
+
+
+
+**The root key is `servers`, not `mcpServers`.** VS Code uses a different schema from Cursor, Claude, and most other clients. Copying a `mcpServers` snippet from elsewhere silently fails to register the server.
+
+
+
+**Local server:** Replace the URL with your own HTTP endpoint. Everything else stays the same.
+
+
+## Step 3: Verify the Connection
+
+Run **MCP: List Servers** from the command palette. The `prowler` server should appear as running.
+
+
+
+
+
+Select the server to start, stop, or restart it, and to view its output log if the connection fails.
+
+## Step 4: Start Using Prowler MCP
+
+Open the Chat view and switch the mode selector to **Agent**. Click the tools icon to confirm the Prowler tools are available, then ask:
+
+- *"Show me all critical findings from my AWS accounts"*
+- *"What does the S3 bucket public access check do?"*
+- *"Summarize my CIS compliance status by provider"*
+
+
+
+
+
+Copilot asks for confirmation before running an MCP tool for the first time.
+
+## Troubleshooting
+
+### Server Does Not Appear
+
+- Confirm the root key is `servers`, not `mcpServers`.
+- Confirm each server entry has `"type": "http"`.
+- Check that `mcp.json` is valid JSON.
+- Verify your VS Code version is 1.102 or later.
+
+### Authentication Fails With 401
+
+- Verify the header value includes the `Bearer ` prefix.
+- Confirm the key has not been revoked in Prowler Cloud.
+- If you mistyped the key at the prompt, run **MCP: List Servers**, select `prowler`, and restart it to be prompted again.
+
+### Tools Do Not Appear in Chat
+
+- Make sure the Chat view is in **Agent** mode. MCP tools are not available in Ask mode.
+- Open the tools picker and confirm the Prowler tools are enabled.
+
+## Next Steps
+
+
+
+ Explore all available tools and capabilities
+
+
+ Configuration reference for every supported client
+
+
+
+## Getting Help
+
+- Search for existing [GitHub issues](https://github.com/prowler-cloud/prowler/issues)
+- Ask for help in our [Slack community](https://goto.prowler.com/slack)
+- Report a new issue on [GitHub](https://github.com/prowler-cloud/prowler/issues/new)
diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx
index 870e17a913..c6f74efb28 100644
--- a/docs/user-guide/cli/tutorials/configuration_file.mdx
+++ b/docs/user-guide/cli/tutorials/configuration_file.mdx
@@ -64,6 +64,7 @@ The following list includes all the AWS checks with configurable variables that
| `dynamodb_table_cross_account_access` | `trusted_account_ids` | List of Strings | `[]` |
| `ec2_elastic_ip_shodan` | `shodan_api_key` | String | `null` |
| `ec2_instance_older_than_specific_days` | `max_ec2_instance_age_in_days` | Integer | `180` |
+| `ec2_instance_stopped_older_than_specific_days` | `max_ec2_instance_stopped_days` | Integer | `30` |
| `ec2_instance_secrets_user_data` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `ec2_launch_template_no_secrets` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings | `["amazon-elb"]` |
@@ -79,6 +80,7 @@ The following list includes all the AWS checks with configurable variables that
| `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer | `7` |
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer | `2` |
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | `2` |
+| `elbv2_listener_pqc_tls_enabled` | `elbv2_listener_pqc_tls_allowed_policies` | List of Strings | See `config.yaml` |
| `eventbridge_bus_cross_account_access` | `trusted_account_ids` | List of Strings | `[]` |
| `eventbridge_schema_registry_cross_account_access` | `trusted_account_ids` | List of Strings | `[]` |
| `glue_etl_jobs_no_secrets_in_arguments` | `secrets_ignore_patterns` | List of Strings | `[]` |
@@ -392,6 +394,8 @@ aws:
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
+ # aws.ec2_instance_stopped_older_than_specific_days --> by default is 30 days
+ max_ec2_instance_stopped_days: 30
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
diff --git a/docs/user-guide/cli/tutorials/custom-checks-metadata.mdx b/docs/user-guide/cli/tutorials/custom-checks-metadata.mdx
index 5d365173a8..77f5bbd3dd 100644
--- a/docs/user-guide/cli/tutorials/custom-checks-metadata.mdx
+++ b/docs/user-guide/cli/tutorials/custom-checks-metadata.mdx
@@ -5,13 +5,13 @@ description: "Use --custom-checks-metadata-file in Prowler CLI to override defau
In certain organizations, the severity of specific checks might differ from the default values defined in the check's metadata. For instance, while `s3_bucket_level_public_access_block` could be deemed `critical` for some organizations, others might assign a different severity level to it.
-The custom metadata option offers a means to override default metadata set by Prowler
+The custom metadata option offers a means to override default metadata set by Prowler.
You can utilize `--custom-checks-metadata-file` followed by the path to your custom checks metadata YAML file.
## Available Fields
-The list of supported check's metadata fields that can be override are listed as follows:
+The list of supported check's metadata fields that can be overridden are listed as follows:
- Severity
- CheckTitle
diff --git a/docs/user-guide/cli/tutorials/dashboard.mdx b/docs/user-guide/cli/tutorials/dashboard.mdx
index 0f20a69939..0b22d92a9b 100644
--- a/docs/user-guide/cli/tutorials/dashboard.mdx
+++ b/docs/user-guide/cli/tutorials/dashboard.mdx
@@ -3,7 +3,7 @@ title: "Run the Prowler local dashboard from CSV outputs"
description: "Launch the built-in Prowler dashboard to visualize CSV scan output locally or in Docker, exposing an interactive UI on port 11666 for review."
---
-Prowler allows you to run your own local dashboards using the csv outputs provided by Prowler
+Prowler Local Dashboard is a local web dashboard built from the CSV outputs produced by Prowler CLI. Launch it with:
```sh
prowler dashboard
@@ -13,7 +13,7 @@ prowler dashboard
You can expose the `dashboard` server in another address using the `HOST` environment variable.
-To run Prowler local dashboard with Docker, use:
+To run Prowler Local Dashboard with Docker, use:
```sh
docker run -v /your/local/dir/prowler-output:/home/prowler/output --env HOST=0.0.0.0 --publish 127.0.0.1:11666:11666 toniblyx/prowler:latest dashboard
@@ -37,7 +37,7 @@ This page allows for multiple functions:
- Apply filters:
- - Assesment Date
+ - Assessment Date
- Account
- Region
- Severity
@@ -46,7 +46,7 @@ This page allows for multiple functions:
- Status
- Category
-- See which files has been scanned to generate the dashboard by placing your mouse on the `?` icon:
+- See which files have been scanned to generate the dashboard by placing your mouse on the `?` icon:
{" "}
@@ -68,7 +68,7 @@ This page shows all the info related to the compliance selected. Multiple filter
To add your own compliance to compliance page, add a file with the compliance name (using `_` instead of `.`) to the path `/dashboard/compliance`.
-In this file use the format present in the others compliance files to create the table. Example for CIS 2.0:
+In this file use the format present in the other compliance files to create the table. Example for CIS 2.0:
```python
import warnings
diff --git a/docs/user-guide/cli/tutorials/integrations.mdx b/docs/user-guide/cli/tutorials/integrations.mdx
index 49689fa9fc..0bfaa4b703 100644
--- a/docs/user-guide/cli/tutorials/integrations.mdx
+++ b/docs/user-guide/cli/tutorials/integrations.mdx
@@ -34,9 +34,9 @@ To configure the Slack Integration, follow the next steps:
2. Optionally, create a Slack Channel (you can use an existing one)
3. Integrate the created Slack App to your Slack channel:
- - Click on the channel, go to the Integrations tab, and Add an App.
+ - Click the channel, go to the Integrations tab, and Add an App.

4. Set the following environment variables that Prowler will read:
- - `SLACK_API_TOKEN`: the *Slack App OAuth Token* that was previously get.
+ - `SLACK_API_TOKEN`: the *Slack App OAuth Token* that was previously obtained.
- `SLACK_CHANNEL_NAME`: the name of your Slack Channel where Prowler will send the message.
diff --git a/docs/user-guide/cli/tutorials/logging.mdx b/docs/user-guide/cli/tutorials/logging.mdx
index f527f36f4c..771ffc8ee4 100644
--- a/docs/user-guide/cli/tutorials/logging.mdx
+++ b/docs/user-guide/cli/tutorials/logging.mdx
@@ -3,7 +3,7 @@ title: 'Prowler CLI logging levels and log file output'
description: 'Configure Prowler CLI log levels with --log-level and route DEBUG, INFO, WARNING, ERROR, or CRITICAL messages to a file with --log-file for troubleshooting.'
---
-Prowler has a logging feature to be as transparent as possible, so that you can see every action that is being performed whilst the tool is being executing.
+Prowler has a logging feature to be as transparent as possible, so that you can see every action that is being performed whilst the tool is being executed.
## Set Log Level
diff --git a/docs/user-guide/cli/tutorials/mutelist.mdx b/docs/user-guide/cli/tutorials/mutelist.mdx
index 95d60ce3f2..e03c76dee3 100644
--- a/docs/user-guide/cli/tutorials/mutelist.mdx
+++ b/docs/user-guide/cli/tutorials/mutelist.mdx
@@ -319,11 +319,11 @@ The DynamoDB Table must have the following String keys:
The Mutelist Table must have the following columns:
- - Accounts (String): This field can contain either an Account ID or an `*` (which applies to all the accounts that use this table as an mutelist).
+ - Accounts (String): This field can contain either an Account ID or an `*` (which applies to all the accounts that use this table as a mutelist).
- Checks (String): This field can contain either a Prowler Check Name or an `*` (which applies to all the scanned checks).
- - Regions (List): This field contains a list of regions where this mutelist rule is applied (it can also contains an `*` to apply all scanned regions).
+ - Regions (List): This field contains a list of regions where this mutelist rule is applied (it can also contain an `*` to apply all scanned regions).
- Resources (List): This field contains a list of regular expressions (regex) that applies to the resources that are wanted to be muted.
@@ -359,7 +359,7 @@ Make sure that the credentials that Prowler uses can invoke the Lambda Function:
Resource: arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME
```
-The Lambda Function can then generate an Mutelist dynamically. Here is the code an example Python Lambda Function that generates an Mutelist:
+The Lambda Function can then generate a Mutelist dynamically. Here is the code of an example Python Lambda Function that generates a Mutelist:
```
def handler(event, context):
diff --git a/docs/user-guide/cli/tutorials/parallel-execution.mdx b/docs/user-guide/cli/tutorials/parallel-execution.mdx
index 32bb14812f..9984c4f310 100644
--- a/docs/user-guide/cli/tutorials/parallel-execution.mdx
+++ b/docs/user-guide/cli/tutorials/parallel-execution.mdx
@@ -134,9 +134,9 @@ Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - All jobs completed"
Output will be stored in `C:\Users\YOUR-USER\Documents\output\`
-## Combining the output files
+## Combining the Output Files
-Guidance is provided for the CSV file format. From the ouput directory, execute either the following Bash or PowerShell script. The script will collect the output from the CSV files, only include the header from the first file, and then output the result as CombinedCSV.csv in the current working directory.
+Guidance is provided for the CSV file format. From the output directory, execute either the following Bash or PowerShell script. The script will collect the output from the CSV files, only include the header from the first file, and then output the result as CombinedCSV.csv in the current working directory.
There is no logic implemented in terms of which CSV files it will combine. If you have additional CSV files from other actions, such as running a quick inventory, you will need to move that out of the current (or any nested) directory, or move the output you want to combine into its own folder and run the script from there.
@@ -185,7 +185,7 @@ $combinedCsv | Export-Csv -Path "CombinedCSV.csv" -NoTypeInformation
## TODO: Additional Improvements
-Some services need to instantiate another service to perform a check. For instance, `cloudwatch` will instantiate Prowler's `iam` service to perform the `cloudwatch_cross_account_sharing_disabled` check. When the `iam` service is instantiated, it will perform the `__init__` function, and pull all the information required for that service. This provides an opportunity for an improvement in the above script to group related services together so that the `iam` services (or any other cross-service references) isn't repeatedily instantiated by grouping dependant services together. A complete mapping between these services still needs to be further investigated, but these are the cross-references that have been noted:
+Some services need to instantiate another service to perform a check. For instance, `cloudwatch` will instantiate Prowler's `iam` service to perform the `cloudwatch_cross_account_sharing_disabled` check. When the `iam` service is instantiated, it will perform the `__init__` function, and pull all the information required for that service. This provides an opportunity for an improvement in the above script to group related services together so that the `iam` services (or any other cross-service references) aren't repeatedly instantiated by grouping dependent services together. A complete mapping between these services still needs to be further investigated, but these are the cross-references that have been noted:
* inspector2 needs lambda and ec2
* cloudwatch needs iam
diff --git a/docs/user-guide/cli/tutorials/quick-inventory.mdx b/docs/user-guide/cli/tutorials/quick-inventory.mdx
index 23eab4f1c9..2c3f267245 100644
--- a/docs/user-guide/cli/tutorials/quick-inventory.mdx
+++ b/docs/user-guide/cli/tutorials/quick-inventory.mdx
@@ -19,7 +19,7 @@ Currently, it is only available for AWS provider.
By default, it extracts resources from all the regions, you could use `-f`/`--filter-region` to specify the regions to execute the analysis.
-- This feature specify both the number of resources for each service and for each resource type.
+- This feature specifies both the number of resources for each service and for each resource type.
- Also, it creates by default a CSV and JSON to see detailed information about the resources extracted.
diff --git a/docs/user-guide/cli/tutorials/reporting.mdx b/docs/user-guide/cli/tutorials/reporting.mdx
index 8ed69048e7..f79aab333a 100644
--- a/docs/user-guide/cli/tutorials/reporting.mdx
+++ b/docs/user-guide/cli/tutorials/reporting.mdx
@@ -50,7 +50,7 @@ prowler -M csv json-ocsf json-asff -o
Both flags can be used simultaneously to provide a custom directory and filename. `console prowler -M csv json-ocsf json-asff \ -F -o `
-## Output timestamp format
+## Output Timestamp Format
By default, the timestamp format of the output files is ISO 8601. This can be changed with the flag `--unix-timestamp` generating the timestamp fields in pure unix timestamp format.
@@ -115,7 +115,7 @@ The CSV format follows a standardized structure across all providers. The follow
#### CSV Headers Mapping
-The following table shows the mapping between the CSV headers and the the providers fields:
+The following table shows the mapping between the CSV headers and the providers fields:
| Open Source Consolidated| AWS| GCP| AZURE| KUBERNETES
|----------|----------|----------|----------|----------
diff --git a/docs/user-guide/compliance/tutorials/compliance.mdx b/docs/user-guide/compliance/tutorials/compliance.mdx
index 58fbac0c6c..23a3ae0b83 100644
--- a/docs/user-guide/compliance/tutorials/compliance.mdx
+++ b/docs/user-guide/compliance/tutorials/compliance.mdx
@@ -3,12 +3,15 @@ title: 'Prowler compliance frameworks and reports'
description: 'Run Prowler compliance scans against CIS, NIST, ISO 27001, PCI-DSS, SOC 2, and more, then download CSV or PDF reports from Prowler Cloud, App, or CLI.'
---
-Prowler maps every security check to one or more industry-standard compliance frameworks, so a single scan produces both technical findings and framework-aligned evidence. The same evaluation runs identically whether scans are launched from Prowler Cloud, Prowler App, or Prowler CLI.
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+Prowler maps every security check to one or more industry-standard compliance frameworks, so a single scan produces both technical findings and framework-aligned evidence. The same evaluation runs identically whether scans are launched from Prowler Cloud, Prowler Local Server, or Prowler CLI.
Out of the box, Prowler covers frameworks such as CIS Benchmarks, NIST 800-53, NIST CSF, NIS2, ENS RD2022, ISO 27001, PCI-DSS, SOC 2, GDPR, HIPAA, AWS Well-Architected, BSI C5, CSA CCM, MITRE ATT&CK, KISA ISMS-P, FedRAMP, and Prowler ThreatScore. The full catalog is available at [Prowler Hub](https://hub.prowler.com/compliance).
-For the unified compliance score methodology used across frameworks, see [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore).
+For the unified compliance score methodology used across frameworks, see [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore).
@@ -20,23 +23,28 @@ For the unified compliance score methodology used across frameworks, see [Prowle
-## Prowler Cloud
+## Prowler Cloud and Prowler Local Server
-The Compliance section in Prowler Cloud and Prowler App centralizes compliance posture across every connected provider. It aggregates scan results, surfaces Prowler ThreatScore, and exposes detailed requirement-level evidence for each supported framework.
+The Compliance section in Prowler Cloud and Prowler Local Server centralizes compliance posture across every connected provider. It aggregates scan results, surfaces Prowler ThreatScore, and exposes detailed requirement-level evidence for each supported framework.
### Accessing the Compliance Section
To open the compliance overview, follow these steps:
-1. Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) or to a self-hosted Prowler App instance.
+1. Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) or to a Prowler Local Server instance.
2. Select **Compliance** from the left navigation.
-The page lists every framework evaluated by the most recent completed scan of the selected provider.
+The Compliance page is organized into two tabs:
-
+* **Single Scan:** Lists every framework evaluated by one completed scan of one provider. This is the experience described in the rest of this guide, and the default view in Prowler Local Server.
+* **Multiple Scans:** Aggregates one framework across several scans at once, either across provider types ([Cross-Provider Type Compliance](/user-guide/compliance/tutorials/cross-provider-type-compliance)) or across every provider of the same type ([Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance)).
+
+
+
+
-Compliance results require at least one completed scan. If no scan has finished yet, Prowler Cloud and App display a notice prompting to launch or wait for a scan to complete.
+Compliance results require at least one completed scan. If no scan has finished yet, Prowler Cloud and Prowler Local Server display a notice prompting to launch or wait for a scan to complete.
### Filtering Compliance Results
@@ -49,7 +57,7 @@ The scan selector lists completed scans across all connected providers. Each ent
#### Region Filter
-The region multi-select narrows results to one or more regions detected in the selected scan. Use it to evaluate compliance posture for a specific geography or account boundary. The filter applies to:
+The region multi-select narrows results to one or more regions detected in the selected scan. Use it to evaluate compliance posture for a specific geography or regulatory boundary. The filter applies to:
* The framework grid scores and pass/fail counts.
* The detailed requirement view inside each framework.
@@ -74,7 +82,7 @@ When the selected scan includes Prowler ThreatScore data, a dedicated card appea
Selecting the card opens the ThreatScore framework detail page, covered in [Working With the Framework Detail Page](#working-with-the-framework-detail-page).
-For a complete explanation of the methodology, formula, and weighting, see [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore).
+For a complete explanation of the methodology, formula, and weighting, see [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore).
### Exploring the Framework Grid
@@ -94,6 +102,49 @@ Select any card to open the framework detail page.
Score color coding follows three thresholds: red for severely low compliance, amber for partial compliance, and green for healthy posture. Hover over the score for the exact percentage.
+### Tracking Frameworks With the Compliance Watchlist
+
+
+
+
+
+The compliance catalog lists dozens of frameworks, while an organization usually tracks a handful. In Prowler Cloud, Compliance Watchlist keeps that handful in front: pin the frameworks that matter and narrow every compliance surface down to them. The watchlist is shared by the whole organization: one list per tenant, not a per-user bookmark, so every member sees the same pinned frameworks.
+
+#### Pinning a Framework
+
+Every framework card carries a pin button in its top-right corner. Select the pin to add the framework to the watchlist, and select it again to remove it. Pinning is available on the three compliance surfaces:
+
+* **Single Scan:** The framework grid of the selected scan.
+* **Across provider types:** The universal framework cards in the **Multiple Scans** tab.
+* **Across providers:** The framework cards inside each provider type group in the **Multiple Scans** tab.
+
+
+
+
+Universal frameworks (CSA CCM, CIS Controls, DORA) are a single watchlist entry. Pinning one of them from any surface shows it as pinned on the others.
+
+
+#### Filtering With the Watchlist
+
+Two controls sit above the tabs, because both tabs read the same watchlist:
+
+* **Show only watchlist:** A toggle that hides every framework not in the watchlist, on both tabs at once. When the filter leaves a section with nothing to show, the section explains that no pinned framework matches and offers to clear the filter.
+* **Watchlist selector:** A searchable multi-select over the full framework catalog, grouped by provider. Use it to pin or unpin several frameworks in one place instead of visiting each card.
+
+
+
+In Prowler Cloud, the compliance framework chips in the finding details panel follow the watchlist as well, so triage points to the same frameworks the organization tracks.
+
+#### Reviewing the Watchlist on the Overview Page
+
+The **Compliance Watchlist** card on the Overview page lists exactly the pinned frameworks with their current score, computed from the latest completed scan per provider. Selecting an entry opens the framework detail page. Until a framework is pinned, the card is empty and prompts to start pinning from the Compliance section.
+
+
+
+
+In Prowler Local Server, where the watchlist is not available, the card keeps its previous behavior and ranks every framework with scan data.
+
+
### Working With the Framework Detail Page
The detail page provides everything needed to evaluate a single framework: aggregate metrics, top failure sections, and a requirement-by-requirement view.
@@ -144,7 +195,7 @@ Frameworks without a custom layout fall back to the generic details panel, which
### Downloading Compliance Reports
-Prowler Cloud and App expose two formats:
+Prowler Cloud and Prowler Local Server expose two formats:
* **CSV report:** Every requirement, every check, and every finding for the selected scan and filters. Available for all supported frameworks.
* **PDF report:** Curated executive-style report. Currently supported for Prowler ThreatScore, ENS RD2022, NIS2, and CSA CCM. Additional PDF reports are added in subsequent Prowler releases.
@@ -169,7 +220,7 @@ Region filters disable the per-card download dropdown to avoid generating partia
#### Downloading the Full Scan Output
-To export every framework, finding, and resource at once, use the **Scan Jobs** section instead. The ZIP archive contains the CSV, JSON-OCSF, and HTML reports plus a `compliance/` subfolder with one CSV per framework. See [Prowler App — Getting Started](/user-guide/tutorials/prowler-app) for details.
+To export every framework, finding, and resource at once, use the **Scan Jobs** section instead. The ZIP archive contains the CSV, JSON-OCSF, and HTML reports plus a `compliance/` subfolder with one CSV per framework. See [Prowler Cloud — Getting Started](/user-guide/tutorials/prowler-app) for details.
### API Access
@@ -182,7 +233,7 @@ Use the API to integrate compliance evidence into ticketing systems, executive d
## Prowler CLI
-Prowler CLI evaluates the same compliance frameworks as Prowler Cloud and App, and produces detailed CSV outputs alongside the standard scan results. By default, it runs every supported framework and prints a status summary at the end of the scan:
+Prowler CLI evaluates the same compliance frameworks as Prowler Cloud and Prowler Local Server, and produces detailed CSV outputs alongside the standard scan results. By default, it runs every supported framework and prints a status summary at the end of the scan:
@@ -262,6 +313,8 @@ To request a new framework or contribute one, see [Creating a New Security Compl
## Related Documentation
-* [Prowler ThreatScore Documentation](/user-guide/compliance/tutorials/threatscore)
+* [Cross-Provider Type Compliance](/user-guide/compliance/tutorials/cross-provider-type-compliance)
+* [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance)
+* [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore)
* [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework)
-* [Prowler App — Getting Started](/user-guide/tutorials/prowler-app)
+* [Prowler Cloud — Getting Started](/user-guide/tutorials/prowler-app)
diff --git a/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx b/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx
new file mode 100644
index 0000000000..6e8a84810b
--- /dev/null
+++ b/docs/user-guide/compliance/tutorials/cross-provider-compliance.mdx
@@ -0,0 +1,147 @@
+---
+title: 'Cross-Provider Compliance'
+sidebarTitle: 'Cross-Provider Compliance'
+description: 'Aggregate a single-provider compliance framework across every provider of the same type, review the consolidated roll-up and per-provider coverage, and download a combined PDF report.'
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
+
+
+
+Cross-Provider Compliance aggregates one **single-provider compliance framework** — CIS AWS, CIS GCP, ENS for Azure — across every provider of that type into a single view. It answers the question a per-scan report cannot: **"How compliant is my whole AWS estate against CIS AWS, together?"**
+
+
+
+This view is the sibling of [Cross-Provider Type Compliance](/user-guide/compliance/tutorials/cross-provider-type-compliance), which aggregates a *universal* framework across different provider types. Both live in the **Multiple Scans** tab and share the same roll-up rules, scan selection, and report flow. Only the column axis changes, from provider type to individual provider.
+
+## How Cross-Provider Compliance Works
+
+For a chosen framework and provider type, Prowler Cloud:
+
+1. Selects **one scan per provider**: the latest completed scan of every provider of that type you are allowed to see.
+2. Aggregates the requirement results across those scans.
+3. Computes a **roll-up status** for each requirement and an overall pass / fail / manual summary.
+4. Exposes a **per-provider breakdown** so a failing provider is immediately attributable.
+
+## Accessing the Cross-Provider View
+
+
+
+ Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) and select **Compliance** from the left navigation.
+
+
+ **Multiple Scans** is the landing tab of the Compliance page in Prowler Cloud. Scroll to the **Across providers** section, below the **Across provider types** cards.
+
+
+ Each provider type is a collapsible group headed by its counts (frameworks available and providers registered). Expanding it reveals one card per single-provider framework available for that type.
+
+
+
+
+
+
+A provider type appears only when it has two or more providers registered **and** at least one of them has a completed scan: that scan is where the framework catalog of the provider type is read from. With a single provider the aggregation is identical to the standard per-scan [Compliance](/user-guide/compliance/tutorials/compliance) view.
+
+
+Framework cards in this section carry no score: they enumerate which frameworks can be aggregated for a provider type, and the roll-up numbers are computed on the detail page. Two catalogs stay out of the section, because each already has its own view:
+
+* **Universal frameworks:** Aggregated in the **Across provider types** section above. See [Cross-Provider Type Compliance](/user-guide/compliance/tutorials/cross-provider-type-compliance).
+* **Prowler ThreatScore:** Reviewed per scan in the **Single Scan** tab. See [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore).
+
+
+
+## Pinning Frameworks to the Watchlist
+
+
+
+Framework cards inside each provider type group carry a pin button that adds the framework to the organization's [Compliance Watchlist](/user-guide/compliance/tutorials/compliance#tracking-frameworks-with-the-compliance-watchlist). With the **Show only watchlist** toggle enabled, each group lists only its pinned frameworks, and a group whose frameworks are all filtered out explains that no pinned framework matches instead of expanding into an empty accordion.
+
+
+
+## Which Providers Are Listed and Which Contribute
+
+The **Across providers** section and the detail page count different things, so their numbers often differ:
+
+* The section describes your **catalog**: the group header and each framework card report how many providers of that type exist in Prowler Cloud, after the filters you applied.
+* The detail page describes your **evidence**: only providers with a completed scan become a column in the aggregation, because every number on that page is computed from scan results.
+
+A card can therefore read `17 providers` while its detail page reports two providers aggregated from two scans. The other 15 providers exist in Prowler Cloud but have no completed scan, so there is nothing of theirs to aggregate. This is the expected state right after onboarding an AWS Organization: the discovery wizard registers every member it finds in the organization as a provider, and providers not scanned yet count toward the catalog while contributing nothing to the roll-up. See [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations) for that onboarding flow.
+
+What decides whether a provider contributes is **having a completed scan**, not its connection status:
+
+* **Completed scans only:** Failed, cancelled, and in-progress scans are ignored, so a provider whose latest scan is still running keeps contributing its previous completed one.
+* **Disconnected providers still count:** A provider whose credentials stopped working contributes its last completed scan. The posture it shows is as old as that scan.
+* **Newly connected providers do not:** A provider contributes nothing until its first scan completes.
+
+To confirm which providers made it into an aggregation, read the coverage summary in the detail page header and the coverage card, which lists one row per contributing provider.
+
+
+The **Providers** filter on the detail page lists every provider of the type, including ones that have never been scanned. Narrowing to providers with no completed scan leaves the view with no evidence to aggregate: the coverage card reports nothing scanned, requirements show no per-provider status, and any report generated for that selection is empty. Clear the filter or select providers that have already been scanned.
+
+
+## Working With the Framework Detail Page
+
+Selecting a card opens a detail page with the same layout as the cross-provider-type detail, with the column axis swapped from provider type to provider:
+
+* **Header:** States the framework, the provider type, and the real coverage behind the numbers, as a count of aggregated providers and scans. The **Report** button generates the combined PDF.
+* **Requirements Status:** Donut chart with the consolidated `Pass`, `Fail`, and `Manual` counts.
+* **Coverage card:** Ranks each contributing provider's individual posture, so the weakest one is visible at a glance. Every row is one provider of the type, labeled with its alias and unique identifier (UID).
+* **Top Failed Sections:** Ranks the framework sections with the most failing requirements, with deep links into the requirements accordion.
+* **Requirements accordion:** Each requirement shows its roll-up badge plus the status of every contributing provider, labeled with the provider alias and unique identifier (UID). With one or two providers the statuses appear as inline chips; from three onwards the row condenses into per-status counts (for example, `Fail ×3 Pass ×6`), and selecting the counts opens the full provider-by-provider breakdown. Expanding a requirement queries the findings of every contributing scan and merges them into a single table.
+
+
+
+
+Checks are not labeled per provider type as in the cross-provider-type view. Every provider of the same type shares one check set, so a single list of checks covers the whole aggregation.
+
+
+### Filtering the Roll-Up
+
+Two filters control which providers feed the aggregation:
+
+* **Providers:** Narrow to specific providers of the type, listed by alias and UID.
+* **Provider group:** Narrow to the providers belonging to one or more provider groups.
+
+The provider type is fixed by the framework, so there is no type filter here. Filters applied on the overview carry through into the detail page and the PDF report.
+
+## Understanding the Roll-Up Status
+
+Results roll up in two stages, with a strict **FAIL > PASS > MANUAL** precedence.
+
+**Per provider, per requirement:**
+
+* If any check fails → the provider contributes **FAIL** for that requirement.
+* Else if every check passes → **PASS**.
+* Otherwise (no pass/fail evidence) → **MANUAL**.
+
+**Across providers, per requirement (the roll-up badge):**
+
+* If at least one contributing provider is **FAIL** → the requirement is **FAIL**.
+* Else if at least one contributing provider is **PASS** → **PASS**.
+* Otherwise → **MANUAL**.
+
+
+Only providers that **actually contributed a result** for a requirement are counted. A provider whose scan produced no result for a specific requirement does not degrade that requirement to Manual, which keeps the roll-up focused on real evidence.
+
+
+Scan selection and permission scoping match the cross-provider-type view: the aggregation always reflects each provider's most recent completed scan, restricted to the providers your role is allowed to see. To review how provider visibility is granted, see [Role-Based Access Control (RBAC)](/user-guide/tutorials/prowler-app-rbac).
+
+## Downloading the Combined PDF Report
+
+The **Report** button produces a single PDF across every contributing provider of the type: a cover page listing the providers, an executive summary with the consolidated roll-up, charts, a requirements index, and detailed findings grouped by requirement and provider.
+
+
+
+Generation is asynchronous and behaves exactly like the cross-provider-type report — background job, toast notification when ready, and **Report → Download latest** to reuse a report already generated for the current filters. See [Downloading the Combined PDF Report](/user-guide/compliance/tutorials/cross-provider-type-compliance#downloading-the-combined-pdf-report) for the full flow, including report reuse and the findings cap.
+
+
+A report is tied to the exact set of scans it was built from. When any contributing provider completes a new scan, the previous report no longer matches the current selection and Prowler Cloud offers to generate an up-to-date one.
+
+
+## Related Documentation
+
+* [Cross-Provider Type Compliance](/user-guide/compliance/tutorials/cross-provider-type-compliance)
+* [Compliance](/user-guide/compliance/tutorials/compliance)
+* [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations)
+* [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore)
diff --git a/docs/user-guide/compliance/tutorials/cross-provider-type-compliance.mdx b/docs/user-guide/compliance/tutorials/cross-provider-type-compliance.mdx
new file mode 100644
index 0000000000..c7b522cf8b
--- /dev/null
+++ b/docs/user-guide/compliance/tutorials/cross-provider-type-compliance.mdx
@@ -0,0 +1,233 @@
+---
+title: 'Cross-Provider Type Compliance'
+sidebarTitle: 'Cross-Provider Type Compliance'
+description: 'Aggregate a universal compliance framework across every compatible provider with a completed scan, review the consolidated roll-up and per-provider breakdown, and download a combined PDF report.'
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
+
+
+
+Cross-Provider Type Compliance consolidates a single **universal compliance framework** across your compatible providers with completed scans into one unified view. Instead of reviewing the same framework on AWS, Azure, Google Cloud, and other supported providers as separate reports, Prowler takes the most recent completed scan of every compatible provider, aggregates them by requirement, and produces a single roll-up posture with a per-provider breakdown and a combined executive PDF.
+
+
+
+## What Is a Universal Compliance Framework
+
+Most Prowler compliance frameworks target a single provider (for example, CIS AWS or CIS Azure). A **universal** framework declares its requirements once in a single JSON and maps each requirement to checks for many providers at the same time, so the same CSA CCM control maps to both AWS and Azure checks. Universal frameworks live at the top of the compliance catalog (`prowler/compliance/.json`), as opposed to the legacy per-provider frameworks under `prowler/compliance//`. For real definitions, see [`csa_ccm_4.0.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/csa_ccm_4.0.json), [`cis_controls_8.1.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/cis_controls_8.1.json), and [`dora_2022_2554.json`](https://github.com/prowler-cloud/prowler/blob/master/prowler/compliance/dora_2022_2554.json) in the Prowler repository.
+
+Prowler currently ships three universal frameworks: [CSA CCM](https://hub.prowler.com/compliance/csa_ccm_4.0), [CIS Controls](https://hub.prowler.com/compliance/cis_controls_8.1), and [DORA](https://hub.prowler.com/compliance/dora_2022_2554). Each framework page on Prowler Hub lists the full requirement-to-check mapping per provider.
+
+## How Cross-Provider Type Compliance Works
+
+Cross-Provider Type Compliance uses this structure to answer a single question: **"How compliant is my whole estate against this framework, regardless of provider?"** For a chosen universal framework, Prowler Cloud:
+
+1. Selects **one scan per compatible provider**: by default, the latest completed scan of each provider the framework supports and that you are allowed to see.
+2. Aggregates the requirement results across those scans, folding multiple providers of the same type together.
+3. Computes a **roll-up status** for each requirement and an overall pass / fail / manual summary for the framework.
+4. Exposes a **per-provider breakdown** so you can see exactly which provider is failing a given control.
+
+
+Cross-Provider Type Compliance never mixes different frameworks. It aggregates one universal framework at a time across providers. To review a single provider in isolation, use the standard per-scan [Compliance](/user-guide/compliance/tutorials/compliance) view.
+
+
+### Supported Universal Frameworks
+
+The Cross-Provider Type Compliance view currently supports the following universal frameworks. The compatible providers are the ones each framework declares checks for; a provider only contributes to the roll-up when it has a completed scan.
+
+| Framework | Version | Compatible providers |
+|-----------|---------|----------------------|
+| [**CSA CCM**](https://hub.prowler.com/compliance/csa_ccm_4.0) (Cloud Controls Matrix) | 4.0 | AWS, Azure, Google Cloud, Alibaba Cloud, Oracle Cloud |
+| [**CIS Controls**](https://hub.prowler.com/compliance/cis_controls_8.1) | 8.1 | AWS, Azure, Google Cloud, Microsoft 365, Kubernetes, GitHub, Google Workspace, Okta, Oracle Cloud, Alibaba Cloud, Cloudflare, MongoDB Atlas, OpenStack, Vercel |
+| [**DORA**](https://hub.prowler.com/compliance/dora_2022_2554) (Digital Operational Resilience Act) | 2022/2554 | AWS, Azure, Google Cloud, Alibaba Cloud, Cloudflare |
+
+The catalog grows as new universal frameworks ship in Prowler. Browse the full compliance catalog at [Prowler Hub](https://hub.prowler.com/compliance).
+
+## Accessing the Cross-Provider Type View
+
+
+
+ Sign in to Prowler Cloud at [cloud.prowler.com](https://cloud.prowler.com/sign-in) and select **Compliance** from the left navigation.
+
+
+ In Prowler Cloud, **Multiple Scans** is the landing tab of the Compliance page, so it opens already selected. Select **Single Scan** at the top of the page to return to the per-scan compliance experience, which remains unchanged.
+
+
+
+
+The **Across provider types** section requires at least one completed scan for a provider compatible with a universal framework. If none is available, that section shows a notice prompting you to launch or wait for a scan to complete. The **Across providers** section below it has its own requirements — see [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance).
+
+
+## Exploring the Overview
+
+The Multiple Scans tab is organized into two sections, each labeled with the axis it aggregates across:
+
+* **Across provider types:** One card per supported universal framework, aggregating every compatible provider type. This is the Cross-Provider Type Compliance experience described in the rest of this guide.
+* **Across providers:** One card per single-provider framework (for example, CIS AWS) that can be aggregated across every provider of the same type. See [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance).
+
+The **Across provider types** section presents one card per supported universal framework, each summarizing the consolidated posture across every contributing provider.
+
+
+
+Each **framework card** includes:
+
+* **Framework logo, name, and version:** Identifies the universal standard (CSA CCM, CIS Controls, DORA).
+* **Score:** The percentage of passing requirements over the total evaluated, aggregated across every contributing provider. Color coding follows three thresholds: red for severely low compliance, amber for partial compliance, and green for healthy posture.
+* **Passing Requirements:** A `passed / total` counter with the aggregated roll-up.
+* **Provider chips:** One icon per compatible provider. Providers with a completed scan appear active with their passing percentage; providers without a scan appear dimmed with a "no completed scan yet" tooltip so coverage gaps are obvious at a glance.
+* **Failed and manual counts:** The number of failing and manual requirements in the roll-up.
+
+Select any card to open the framework detail page.
+
+### Pinning Universal Frameworks to the Watchlist
+
+
+
+Each universal framework card carries a pin button that adds the framework to the organization's [Compliance Watchlist](/user-guide/compliance/tutorials/compliance#tracking-frameworks-with-the-compliance-watchlist). A universal framework is a single watchlist entry, so pinning it here also shows it as pinned on the Single Scan grid of every compatible provider. The **Show only watchlist** toggle above the tabs narrows both sections of the Multiple Scans tab to the pinned frameworks.
+
+
+
+### Filtering the Roll-Up
+
+The filters bar controls which providers feed every card and detail view. Cross-Provider Type Compliance supports three filters:
+
+* **Provider type:** Narrow the roll-up to specific provider types (for example, only AWS and Azure).
+* **Providers:** Narrow to specific providers, listed by alias and unique identifier (UID).
+* **Provider group:** Narrow to the providers belonging to one or more provider groups.
+
+Select **Clear filters** to reset all filters. Filters applied on the overview are carried through into the detail page and the PDF report so the view stays consistent end to end.
+
+
+Filters narrow **which providers contribute** to the aggregation. They do not change how a requirement rolls up (see [Understanding the Roll-Up Status](#understanding-the-roll-up-status)).
+
+
+## Working With the Framework Detail Page
+
+The detail page provides the full breakdown for a single universal framework: aggregate metrics, provider coverage, top failing sections, and a requirement-by-requirement view with per-provider status.
+
+
+
+### Header
+
+The header shows the framework name and version, a link to the framework page on [Prowler Hub](https://hub.prowler.com/compliance), and a summary such as *"X of Y compatible providers scanned · N scans aggregated"* so you always know the coverage behind the numbers. The **Report** button in the top-right generates and downloads the combined PDF (see [Downloading the Combined PDF Report](#downloading-the-combined-pdf-report)).
+
+### Summary Cards
+
+Below the header, three summary cards condense the framework state:
+
+* **Requirements Status:** Donut chart with `Pass`, `Fail`, and `Manual` counts plus the total number of requirements, reflecting the consolidated roll-up.
+* **Provider Coverage:** Shows which compatible providers contributed a scan and their individual posture, so coverage gaps and per-provider weak spots are visible at a glance.
+* **Top Failed Sections:** Ranks the framework sections with the highest number of failing requirements, with deep links into the requirements accordion.
+
+### Requirements Accordion
+
+The accordion organizes every requirement of the framework. For each requirement you see:
+
+* **Requirement ID and title:** The official identifier from the framework.
+* **Roll-up status badge:** A single `Pass`, `Fail`, or `Manual` badge representing the consolidated status across all contributing providers.
+* **Per-provider status:** The status each contributing provider returned for that requirement, so a single failing provider is immediately attributable. Up to five providers are shown as inline chips; beyond that the row condenses into per-status counts (for example, `Fail ×3 Pass ×6`), and selecting the counts opens the full provider-by-provider breakdown.
+* **Provider-labeled checks:** When you expand a requirement, the underlying checks are labeled with the provider they belong to (each universal requirement maps to different check IDs per provider).
+
+Expand a requirement to review the failing checks per provider, the affected resources, and remediation guidance. Findings are queried across every contributing scan and merged into a single table.
+
+
+
+## Understanding the Roll-Up Status
+
+Cross-Provider Type Compliance rolls up results in two stages, with a strict **FAIL > PASS > MANUAL** precedence.
+
+**Per provider, per requirement:**
+
+* If any check fails → the provider contributes **FAIL** for that requirement.
+* Else if every check passes → **PASS**.
+* Otherwise (no pass/fail evidence) → **MANUAL**.
+
+Multiple providers of the same type (for example, three AWS providers) are folded together first, so a failure in any one of them marks that provider type as failing.
+
+**Across providers, per requirement (the roll-up badge):**
+
+* If at least one contributing provider is **FAIL** → the requirement is **FAIL**.
+* Else if at least one contributing provider is **PASS** → **PASS**.
+* Otherwise → **MANUAL**.
+
+
+Only providers that **actually contributed a result** for a requirement are counted. A provider that has a scan in the aggregation but produced no result for a specific requirement (for example, because the framework maps no checks to that provider for that control) does **not** degrade the requirement to Manual. This keeps the roll-up focused on real evidence.
+
+
+### How Scans Are Selected
+
+By default, Cross-Provider Type Compliance auto-selects the **latest completed scan** of each compatible provider you are allowed to see. This means:
+
+* The view always reflects your most recent posture per provider, without any manual scan selection.
+* Adding a new compatible provider and running a scan automatically brings it into the roll-up.
+* Provider visibility follows your role: only the providers your permissions allow are ever shown, and the roll-up is scoped accordingly. See [Role-Based Access Control (RBAC)](/user-guide/tutorials/prowler-app-rbac).
+
+What makes a provider contribute is **having a completed scan**, not its current connection status:
+
+* Only scans in the **completed** state are eligible. Failed, cancelled, and in-progress scans are ignored, so a provider whose latest scan is still running keeps contributing its previous completed one.
+* A provider whose credentials stopped working still contributes its last completed scan. The posture it shows is as old as that scan.
+* A provider connected but never scanned contributes nothing until its first scan completes. On the framework cards it appears dimmed instead of disappearing, so the coverage gap stays visible.
+
+## Downloading the Combined PDF Report
+
+The **Report** button on the detail page generates a single PDF that combines every contributing provider's latest scan for the framework into one executive document: a cover page listing every contributing provider, an executive summary with the consolidated roll-up, charts, a requirements index, and detailed findings grouped by requirement and provider.
+
+### The Report Follows Your Filters
+
+A report covers the exact set of scans your current filters resolve to. The provider type, providers, and provider group filters applied on the detail page determine which providers contribute, and the auto-select rule pins each contributing provider's latest completed scan. The PDF is built from that resolved scan set together with the framework.
+
+As a result, each filter combination produces its own report. For example:
+
+* No filters → a report covering every compatible provider that has a completed scan.
+* `Provider type = AWS, Azure` → a report covering only your AWS and Azure scans.
+* `Provider group = Production` → a report covering only the providers in that group.
+
+Changing the filters and generating again produces a different, independent report. Each combination is tracked on its own, so switching filters back and forth never overwrites a previously generated report.
+
+
+Region filtering is **not** supported for the combined PDF report. The report recomputes status live across every region of the contributing scans, so a region-scoped request is rejected rather than producing a report that contradicts a region-filtered view.
+
+
+### Generating and Reusing a Report
+
+Because the report aggregates many scans, it is generated **asynchronously**:
+
+
+
+ Select **Report → Generate new report…**, optionally give it a name, and confirm. Prowler starts a background job and shows a "Report generation started" confirmation.
+
+
+ The button shows a "Generating report…" state while the job runs. Generation continues in the background: you can navigate away, and a toast notification appears when the report is ready, even after a page reload.
+
+
+ When the report is ready, select **Download** from the notification, or use **Report → Download latest** at any time to fetch the most recent report for the current filters.
+
+
+
+
+
+A report already generated for a given set of filters does not need to be generated again. When you open the detail page with a filter combination that was reported before, Prowler detects the existing report and surfaces **Report → Download latest** so you can download it immediately, without launching a new job. You only need to generate a fresh report when:
+
+* You apply a filter combination that has never been reported before, or
+* A contributing provider has completed a new scan since the report was generated. The report is tied to the specific scans it was built from, so a newer completed scan makes the previous report stale; Prowler recognizes it no longer matches the current selection and offers to generate an up-to-date one.
+
+"Download latest" reuses an existing report only when it matches the framework, the exact resolved scan set for the current filters, and the same report options. This guarantees the PDF you download reflects the posture you are looking at, rather than a report generated for a different filter or an older scan.
+
+
+The PDF detail section renders only **failed** requirements by default so the report stays focused as an executive/auditor document. As with every Prowler PDF, the detail section is capped at the first 100 failed findings per check; use the per-scan CSV or JSON-OCSF exports for the complete, untruncated list. See [Downloading Compliance Reports](/user-guide/compliance/tutorials/compliance#downloading-compliance-reports) for the full PDF behavior and the `DJANGO_PDF_MAX_FINDINGS_PER_CHECK` setting.
+
+
+## Aggregating a Single-Provider Framework Across Providers
+
+Universal frameworks answer the cross-provider-type question, but most compliance frameworks target a single provider type — CIS AWS, CIS GCP, ENS for Azure. The **Across providers** section of the Multiple Scans tab answers the sibling question for those frameworks: **"How compliant is my whole AWS estate against CIS AWS, together?"**
+
+The aggregation works the same way, with the column axis swapped from provider type to individual provider, and it produces its own combined PDF report. See [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance) for the full guide.
+
+## Related Documentation
+
+* [Cross-Provider Compliance](/user-guide/compliance/tutorials/cross-provider-compliance)
+* [Compliance](/user-guide/compliance/tutorials/compliance)
+* [Prowler ThreatScore](/user-guide/compliance/tutorials/threatscore)
+* [Creating a New Security Compliance Framework in Prowler](/developer-guide/security-compliance-framework)
+* [Prowler Cloud — Getting Started](/user-guide/tutorials/prowler-app)
diff --git a/docs/user-guide/compliance/tutorials/threatscore.mdx b/docs/user-guide/compliance/tutorials/threatscore.mdx
index ff64a06b77..ace76ca4cf 100644
--- a/docs/user-guide/compliance/tutorials/threatscore.mdx
+++ b/docs/user-guide/compliance/tutorials/threatscore.mdx
@@ -3,18 +3,16 @@ title: "Prowler ThreatScore: unified compliance scoring"
description: "Prowler ThreatScore aggregates pass rate, severity, weight, and prevalence into a 0-100 compliance score for tracking cloud security posture over time."
---
-
-
-
## Introduction
-The **Prowler ThreatScore** is a comprehensive compliance scoring system that provides a unified metric for assessing your organization's security posture across compliance frameworks. It aggregates findings from individual security checks into a single, normalized score ranging from 0 to 100.
+Prowler ThreatScore is a comprehensive compliance scoring system that provides a unified metric for assessing security posture across compliance frameworks. It aggregates findings from individual security checks into a single, normalized score ranging from 0 to 100.
### Purpose
-- **Unified View**: Get a single metric representing overall compliance health
-- **Risk Prioritization**: Understand which areas pose the highest security risks
-- **Progress Tracking**: Monitor improvements in compliance posture over time
-- **Executive Reporting**: Provide clear, quantifiable security metrics to stakeholders
+
+- **Unified view:** A single metric represents overall compliance health.
+- **Risk prioritization:** Highlights which areas pose the highest security risks.
+- **Progress tracking:** Monitors improvements in compliance posture over time.
+- **Executive reporting:** Delivers clear, quantifiable security metrics to stakeholders.
## How ThreatScore Works
@@ -30,7 +28,7 @@ Pass Rate = (Number of PASS findings) / (Total findings)
The total number of checks performed (both PASS and FAIL) for a requirement. This represents the amount of evidence available - more findings provide greater confidence in the assessment.
### 3. Weight (`weight_i`)
-A numerical value (1-1000) representing the business importance or criticality of the requirement within your organization's context.
+A numerical value (1-1000) representing the business importance or criticality of the requirement within the organizational context.
### 4. Risk Level (`risk_i`)
A severity rating (1-5) indicating the potential impact of non-compliance with this requirement.
@@ -381,7 +379,7 @@ This comprehensive example demonstrates how:
### Example 4: Impact of Parameter Changes
-Using the scenario, let's see how parameter changes affect the score:
+Using the scenario, the following changes show how parameter adjustments affect the score:
#### Scenario A: Increase Encryption Risk Level
diff --git a/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx b/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx
index 9feb9131ca..f1ba957a1c 100644
--- a/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx
+++ b/docs/user-guide/cookbooks/powerbi-cis-benchmarks.mdx
@@ -14,7 +14,7 @@ The template and its source files live in the Prowler repository under [`contrib
The setup requires the following components:
* **Microsoft Power BI Desktop:** free download from Microsoft.
-* **Prowler compliance CSV exports:** produced by Prowler CLI or downloaded from Prowler Cloud or Prowler App.
+* **Prowler compliance CSV exports:** produced by Prowler CLI or downloaded from Prowler Cloud or Prowler Local Server.
* **Local directory:** holds the CSV exports that the template ingests at load time.
## Supported CIS Benchmarks
@@ -40,7 +40,7 @@ Download and install Microsoft Power BI Desktop from the official Microsoft site
### Step 2: Generate Compliance CSV Exports
-Compliance CSV exports can be generated through Prowler CLI or downloaded from Prowler Cloud and Prowler App.
+Compliance CSV exports can be generated through Prowler CLI or downloaded from Prowler Cloud and Prowler Local Server.
#### Option A: Prowler CLI
@@ -55,7 +55,7 @@ prowler kubernetes --compliance cis_1.12_kubernetes
The compliance CSV exports are written to `output/compliance/` by default.
-#### Option B: Prowler Cloud or Prowler App
+#### Option B: Prowler Cloud or Prowler Local Server
Open the Compliance section, select the desired CIS Benchmark, and download the CSV export.
@@ -160,7 +160,7 @@ A full walkthrough is available on YouTube:
- Review the Compliance workflow across Prowler Cloud, Prowler App, and Prowler CLI.
+ Review the Compliance workflow across Prowler Cloud, Prowler Local Server, and Prowler CLI.
Explore the built-in local dashboard for Prowler CSV exports.
diff --git a/docs/user-guide/img/add-registry-url.png b/docs/user-guide/img/add-registry-url.png
index df1319ea15..eae438fdbf 100644
Binary files a/docs/user-guide/img/add-registry-url.png and b/docs/user-guide/img/add-registry-url.png differ
diff --git a/docs/user-guide/img/image-authentication-filters.png b/docs/user-guide/img/image-authentication-filters.png
index da56306e83..af198c45b5 100644
Binary files a/docs/user-guide/img/image-authentication-filters.png and b/docs/user-guide/img/image-authentication-filters.png differ
diff --git a/docs/user-guide/img/select-container-registry.png b/docs/user-guide/img/select-container-registry.png
index 50fd9c3177..d07faaa768 100644
Binary files a/docs/user-guide/img/select-container-registry.png and b/docs/user-guide/img/select-container-registry.png differ
diff --git a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
index 4da7f04e32..c2c55921f9 100644
--- a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
+++ b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
@@ -33,14 +33,14 @@ Before you begin, make sure you have:
### Step 1: Get Your Alibaba Cloud Account ID
1. Log in to the [Alibaba Cloud Console](https://home.console.alibabacloud.com/)
-2. Click on your profile avatar in the top-right corner
+2. Click your profile avatar in the top-right corner
3. Locate and copy your Account ID

### Step 2: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Providers"

diff --git a/docs/user-guide/providers/aws/authentication.mdx b/docs/user-guide/providers/aws/authentication.mdx
index 896d732ef1..42849a4c28 100644
--- a/docs/user-guide/providers/aws/authentication.mdx
+++ b/docs/user-guide/providers/aws/authentication.mdx
@@ -8,9 +8,9 @@ Prowler requires AWS credentials to function properly. Authentication is availab
- Static Credentials
- Assumed Role
-When using **Assumed Role**, the Prowler UI exposes two credential sources for calling `sts:AssumeRole`. The labels differ between Prowler Cloud and self-hosted Prowler App, but both map to the same underlying credential types:
+When using **Assumed Role**, the Prowler UI exposes two credential sources for calling `sts:AssumeRole`. The labels differ between Prowler Cloud and Prowler Local Server, but both map to the same underlying credential types:
-- **AWS SDK Default** (shown as *"Prowler Cloud will assume your IAM role"* in Prowler Cloud and *"AWS SDK Default"* in self-hosted Prowler App): Prowler uses the credentials already available to the API and worker containers through the [AWS SDK default credential chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html). This is the default in Prowler Cloud and requires extra configuration in self-hosted Prowler App (see [Configuring AWS SDK Default for Self-Hosted Prowler App](#configuring-aws-sdk-default-for-self-hosted-prowler-app)).
+- **AWS SDK Default** (shown as *"Prowler Cloud will assume your IAM role"* in Prowler Cloud and *"AWS SDK Default"* in Prowler Local Server): Prowler uses the credentials already available to the API and worker containers through the [AWS SDK default credential chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html). This is the default in Prowler Cloud and requires extra configuration in Prowler Local Server (see [Configuring AWS SDK Default for Prowler Local Server](#configuring-aws-sdk-default-for-prowler-local-server)).
- **Access & Secret Key**: You paste an IAM user's `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optionally `AWS_SESSION_TOKEN` into the form. Prowler uses those keys to call `sts:AssumeRole`.
## Required Permissions
@@ -82,9 +82,9 @@ This method grants permanent access and is the recommended setup for production
---
-## Configuring AWS SDK Default for Self-Hosted Prowler App
+## Configuring AWS SDK Default for Prowler Local Server
-When self-hosting Prowler App with Docker Compose, the API and worker containers do not have AWS credentials by default. Selecting **AWS SDK Default** without configuring those credentials produces:
+When running Prowler Local Server with Docker Compose, the API and worker containers do not have AWS credentials by default. Selecting **AWS SDK Default** without configuring those credentials produces:
```
AWSAssumeRoleError[1012]: AWS assume role error - An error occurred (InvalidClientTokenId) when calling the AssumeRole operation: The security token included in the request is invalid.
@@ -117,7 +117,7 @@ docker compose up -d --force-recreate api worker worker-beat
### Option 2: IAM Role (Host with Instance Metadata)
-If you run Prowler App on an EC2 instance, ECS task, or EKS pod with an attached IAM role that can assume the scan role, no extra configuration is needed — `boto3` resolves credentials through instance or task metadata automatically.
+If you run Prowler Local Server on an EC2 instance, ECS task, or EKS pod with an attached IAM role that can assume the scan role, no extra configuration is needed — `boto3` resolves credentials through instance or task metadata automatically.
### Trust Policy: Align `IAMPrincipal` With Your Identity
diff --git a/docs/user-guide/providers/aws/boto3-configuration.mdx b/docs/user-guide/providers/aws/boto3-configuration.mdx
index cbac09b0e6..9afb936acc 100644
--- a/docs/user-guide/providers/aws/boto3-configuration.mdx
+++ b/docs/user-guide/providers/aws/boto3-configuration.mdx
@@ -50,6 +50,6 @@ For testing or modifying Prowler's behavior, use the following steps to confirm
* Run prowler with `--log-level DEBUG` and `--log-file debuglogs.txt`
* Search for retry attempts using `grep -i 'Retry needed' debuglogs.txt`
-This approach follows the [AWS documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#checking-retry-attempts-in-your-client-logs), which states that if a retry is performed, a message starting with "Retry needed” will be prompted.
+This approach follows the [AWS documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#checking-retry-attempts-in-your-client-logs), which states that if a retry is performed, a message starting with "Retry needed" will be prompted.
It is possible to determine the total number of calls made using `grep -i 'Sending http request' debuglogs.txt | wc -l`
diff --git a/docs/user-guide/providers/aws/cloudshell.mdx b/docs/user-guide/providers/aws/cloudshell.mdx
index 4befdd21ae..4a43c72476 100644
--- a/docs/user-guide/providers/aws/cloudshell.mdx
+++ b/docs/user-guide/providers/aws/cloudshell.mdx
@@ -3,7 +3,7 @@ title: 'Install and Run Prowler in AWS CloudShell'
description: 'Install Prowler in AWS CloudShell on Amazon Linux 2023, run AWS security scans, and download output reports directly from the browser shell.'
---
-## Following the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023
+## Following the Migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023
AWS CloudShell has migrated from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html). With this transition, Python 3.9 is now included by default in AL2023, eliminating the need for manual compilation.
diff --git a/docs/user-guide/providers/aws/getting-started-aws.mdx b/docs/user-guide/providers/aws/getting-started-aws.mdx
index 0c1914d235..61ff246d86 100644
--- a/docs/user-guide/providers/aws/getting-started-aws.mdx
+++ b/docs/user-guide/providers/aws/getting-started-aws.mdx
@@ -19,7 +19,7 @@ description: 'Onboard an AWS account to Prowler Cloud: locate your account ID, a
### Step 2: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Providers"

@@ -68,7 +68,7 @@ This method grants permanent access and is the recommended setup for production
For detailed instructions on how to create the role, see [Authentication > Assume Role](/user-guide/providers/aws/authentication#assume-role-recommended).
-7. Once the role is created, go to the **IAM Console**, click on the "ProwlerScan" role to open its details:
+7. Once the role is created, go to the **IAM Console**, click the "ProwlerScan" role to open its details:

@@ -76,13 +76,13 @@ For detailed instructions on how to create the role, see [Authentication > Assum

-9. Paste the ARN into the corresponding field in Prowler Cloud or Prowler App
+9. Paste the ARN into the corresponding field in Prowler Cloud or Prowler Local Server

10. Select the credential source Prowler should use to call `sts:AssumeRole`. The option label differs between deployments but both map to the same `aws-sdk-default` credential type:
- - **"Prowler Cloud will assume your IAM role"** (default in Prowler Cloud) / **"AWS SDK Default"** (in self-hosted Prowler App): Prowler uses the credentials available in the API and worker environment through the [AWS SDK default credential chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html). In self-hosted Prowler App, these containers have no AWS credentials by default — see [Configuring AWS SDK Default for Self-Hosted Prowler App](/user-guide/providers/aws/authentication#configuring-aws-sdk-default-for-self-hosted-prowler-app) before choosing this option, or the connection test will fail with `InvalidClientTokenId`.
+ - **"Prowler Cloud will assume your IAM role"** (default in Prowler Cloud) / **"AWS SDK Default"** (in Prowler Local Server): Prowler uses the credentials available in the API and worker environment through the [AWS SDK default credential chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html). In Prowler Local Server, these containers have no AWS credentials by default — see [Configuring AWS SDK Default for Prowler Local Server](/user-guide/providers/aws/authentication#configuring-aws-sdk-default-for-prowler-local-server) before choosing this option, or the connection test will fail with `InvalidClientTokenId`.
- **Access & Secret Key**: Paste an IAM user's `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (and optional `AWS_SESSION_TOKEN`) into the form. The IAM principal must be allowed to assume the target role and must match the `IAMPrincipal` parameter of the scan role template (default: `role/prowler*`).
11. Click "Next", then "Launch Scan"
@@ -111,7 +111,7 @@ AWS accounts can also be configured using static credentials (not recommended fo
For detailed instructions on how to create the credentials, see [Authentication > Credentials](/user-guide/providers/aws/authentication#credentials).
-1. Complete the form in Prowler Cloud or Prowler App and click "Next"
+1. Complete the form in Prowler Cloud or Prowler Local Server and click "Next"

diff --git a/docs/user-guide/providers/aws/img/select-auth-method.png b/docs/user-guide/providers/aws/img/select-auth-method.png
index 17d26dec41..97a8a60016 100644
Binary files a/docs/user-guide/providers/aws/img/select-auth-method.png and b/docs/user-guide/providers/aws/img/select-auth-method.png differ
diff --git a/docs/user-guide/providers/aws/multiaccount.mdx b/docs/user-guide/providers/aws/multiaccount.mdx
index 39a1c0e80d..94dee5bcca 100644
--- a/docs/user-guide/providers/aws/multiaccount.mdx
+++ b/docs/user-guide/providers/aws/multiaccount.mdx
@@ -3,7 +3,7 @@ title: 'Scan Multiple AWS Accounts with Prowler'
description: 'Run Prowler across many AWS accounts sequentially or in parallel using IAM assume role, ideal for auditing several accounts from one entry point.'
---
-Prowler enables security scanning across multiple AWS accounts by utilizing the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
+Prowler enables security scanning across multiple AWS accounts by utilizing the [Assume Role feature](/user-guide/providers/aws/role-assumption) and [integration with AWS Organizations feature](/user-guide/providers/aws/organizations).
This approach allows execution from a single account with permissions to assume roles in the target accounts.
@@ -17,7 +17,7 @@ To scan specific accounts one at a time:
ACCOUNTS_LIST='11111111111 2222222222 333333333'
```
-- Run Prowler with an IAM role that exists in all target accounts: (replace the `` with to yours, that is to be consistent throughout all accounts):
+- Run Prowler with an IAM role that exists in all target accounts: (replace the `` with yours, that is to be consistent throughout all accounts):
```
ROLE_TO_ASSUME=
@@ -36,7 +36,7 @@ Define the AWS accounts to be scanned with a variable:
ACCOUNTS_LIST='11111111111 2222222222 333333333'
```
-- Run Prowler with an IAM role that exists in all target accounts: (replace the `` with to yours, that is to be consistent throughout all accounts). The following example executes scanning across three accounts in parallel:
+- Run Prowler with an IAM role that exists in all target accounts: (replace the `` with yours, that is to be consistent throughout all accounts). The following example executes scanning across three accounts in parallel:
```
ROLE_TO_ASSUME=
diff --git a/docs/user-guide/providers/aws/organizations.mdx b/docs/user-guide/providers/aws/organizations.mdx
index ebb4cad1f2..08e9272204 100644
--- a/docs/user-guide/providers/aws/organizations.mdx
+++ b/docs/user-guide/providers/aws/organizations.mdx
@@ -3,10 +3,12 @@ title: 'AWS Organizations Integration with Prowler'
description: 'Integrate Prowler with AWS Organizations to auto-discover member accounts, enrich findings with account metadata, and deploy scan roles via StackSets.'
---
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
**Using Prowler Cloud?** You can onboard your entire AWS Organization through the UI with automatic account discovery, OU-aware tree selection, and bulk connection testing — no scripts or YAML files required.
-See [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations) for the full walkthrough.
+See [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations) in Prowler Cloud for the full walkthrough.
Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally.
@@ -72,11 +74,43 @@ The additional fields in CSV header output are as follows:
## Deploying Prowler IAM Roles Across AWS Organizations
+
+
When onboarding multiple AWS accounts into Prowler Cloud, it is important to deploy the Prowler Scan IAM Role in each account. The most efficient way to do this across an AWS Organization is by leveraging AWS CloudFormation StackSets, which rolls out infrastructure—like IAM roles—to all accounts centrally from the Management or Delegated Admin account.
-When using Infrastructure as Code (IaC), Terraform is recommended to manage this deployment systematically.
+### Native CloudFormation StackSet Deployment (Recommended)
-### Recommended Approach
+The [Prowler Scan IAM Role CloudFormation template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml) can deploy the role across your entire AWS Organization on its own—no third-party modules required. When launched in the **Management Account** (or a **Delegated Administrator** account) with `DeployStackSet=true` and `EnableOrganizations=true`, it creates a service-managed CloudFormation StackSet that rolls the ProwlerScan role out to every account under the target Organizational Unit (or the organization root), and keeps new accounts covered automatically through auto-deployment.
+
+To deploy from the CloudFormation console: open **CloudFormation → Create stack → With new resources**, choose **Upload a template file** and select `prowler-scan-role.yml` (or paste its S3 URL), then set the parameters below on the **Specify stack details** step. Leave the **Configure stack options** step at its defaults.
+
+Deploy a single CloudFormation Stack in the Management Account with the following parameters:
+
+| Parameter | Description | Default |
+| --- | --- | --- |
+| `ExternalId` | External ID provided by Prowler Cloud to secure role assumption. | — |
+| `DeployLocalRole` | Create the ProwlerScan role in this (Management) account. | `true` |
+| `DeployStackSet` | Create a service-managed StackSet that deploys the role to member accounts. | `false` |
+| `AWSOrganizationalUnitId` | Target OU (`ou-xxxx-yyyyyyyy`) or organization root (`r-xxxx`) for the StackSet. Required when `DeployStackSet=true`. | `""` |
+| `DeployFromDelegatedAdmin` | Set to `true` when deploying from a Delegated Administrator account instead of the Management Account (uses `CallAs: DELEGATED_ADMIN`). | `false` |
+| `EnableOrganizations` | Add AWS Organizations permissions to the Management Account role: read-only account discovery plus the StackSet-management permissions the deployment needs. Set to `true` when deploying in the Management Account. | `false` |
+| `FailureTolerancePercentage` | Percentage of accounts in which the StackSet operation can fail before CloudFormation stops the operation. | `10` |
+| `RetainStacksOnAccountRemoval` | Keep the role in an account after it leaves the Organization or OU. | `false` |
+
+
+On the review step, select **"I acknowledge that AWS CloudFormation might create IAM resources with custom names"** — the template provisions the named `ProwlerScan` IAM role, so the stack requires the `CAPABILITY_NAMED_IAM` capability and fails without this acknowledgment. (The quick-create link handles this for you.)
+
+
+
+The service-managed StackSet does **not** deploy to the Management Account itself. Keeping `DeployLocalRole=true` ensures the role also exists there, so a single stack covers both the Management and member accounts.
+
+Trusted access for CloudFormation StackSets must be enabled in the Organization (see the note at the top of this page) before `DeployStackSet` will work.
+
+Deploying for the CLI or a self-hosted Prowler (not Prowler Cloud)? Also set `AccountId` to the account you assume the role from and `IAMPrincipal` to your identity — the defaults target Prowler Cloud. See [Aligning the trust policy with your identity](/user-guide/providers/aws/authentication#trust-policy-align-iamprincipal-with-your-identity).
+
+
+
+### Alternative: Deploy with Terraform
- **Use StackSets** from the **Management Account** (or a Delegated Admin/Security Account).
- **Use Terraform** to orchestrate the deployment.
diff --git a/docs/user-guide/providers/aws/regions-and-partitions.mdx b/docs/user-guide/providers/aws/regions-and-partitions.mdx
index a9c68f2f07..9cbbcaa0bf 100644
--- a/docs/user-guide/providers/aws/regions-and-partitions.mdx
+++ b/docs/user-guide/providers/aws/regions-and-partitions.mdx
@@ -65,7 +65,7 @@ When more than one source is set, precedence is:
3. `aws.disallowed_regions` in `config.yaml`
-For self-hosted App or API-triggered scans, set `PROWLER_AWS_DISALLOWED_REGIONS` in the runtime environment of the backend scan containers such as `api` and `worker`. The `ui` container does not enforce AWS region selection.
+For Prowler Local Server or API-triggered scans, set `PROWLER_AWS_DISALLOWED_REGIONS` in the runtime environment of the backend scan containers such as `api` and `worker`. The `ui` container does not enforce AWS region selection.
diff --git a/docs/user-guide/providers/aws/role-assumption.mdx b/docs/user-guide/providers/aws/role-assumption.mdx
index ee06b0bc4b..e6e45d5bd2 100644
--- a/docs/user-guide/providers/aws/role-assumption.mdx
+++ b/docs/user-guide/providers/aws/role-assumption.mdx
@@ -78,6 +78,15 @@ The template requires the following parameters:
- **AccountId:** *(Optional)* AWS Account ID that will assume the role (default: Prowler Cloud account)
- **IAMPrincipal:** *(Optional)* The IAM principal allowed to assume the role (default: `role/prowler*`)
+
+From the CLI you assume the role with **your own** identity, not from Prowler Cloud. The `AccountId` and `IAMPrincipal` defaults target Prowler Cloud, so set **`AccountId`** to the account you run Prowler from and **`IAMPrincipal`** to your identity (for example `role/` or `user/`). Otherwise `sts:AssumeRole` fails with `AccessDenied`. See [Aligning the trust policy with your identity](/user-guide/providers/aws/authentication#trust-policy-align-iamprincipal-with-your-identity).
+
+
+
+To deploy the role across an entire AWS Organization from a single stack (Management Account role plus a service-managed StackSet for the member accounts), the template also accepts `DeployLocalRole`, `DeployStackSet`, `AWSOrganizationalUnitId`, `DeployFromDelegatedAdmin`, `EnableOrganizations`, `FailureTolerancePercentage`, and `RetainStacksOnAccountRemoval`. See [AWS Organizations in Prowler](/user-guide/providers/aws/organizations#native-cloudformation-stackset-deployment-recommended) for the full parameter reference.
+
+
+
When running Prowler CLI, include the External ID using the `-I/--external-id` flag:
```sh
diff --git a/docs/user-guide/providers/aws/securityhub.mdx b/docs/user-guide/providers/aws/securityhub.mdx
index c119764411..9f313141b4 100644
--- a/docs/user-guide/providers/aws/securityhub.mdx
+++ b/docs/user-guide/providers/aws/securityhub.mdx
@@ -3,7 +3,7 @@ title: 'AWS Security Hub Integration with Prowler'
description: 'Send Prowler findings to AWS Security Hub by enabling the integration per region, then push results into ASFF for centralized security posture.'
---
-Prowler natively supports **official integration** with [AWS Security Hub](https://aws.amazon.com/security-hub), allowing security findings to be sent directly. This integration enables **Prowler** to import its findings into AWS Security Hub.
+Prowler natively supports **official integration** with [AWS Security Hub](https://aws.amazon.com/security-hub), allowing security findings to be sent directly. This integration enables **Prowler** to import its findings into AWS Security Hub.
To activate the integration, follow these steps in at least one AWS region within your AWS account:
@@ -79,7 +79,7 @@ aws securityhub enable-import-findings-for-product --region eu-west-1 --product-
```
-Specify the AWS region where you want to enable the integration. Ensure the region is correctly set within the ARN value. This command requires the`securityhub:securityhub:EnableImportFindingsForProduct` permission.
+Specify the AWS region where you want to enable the integration. Ensure the region is correctly set within the ARN value. This command requires the `securityhub:EnableImportFindingsForProduct` permission.
## Sending Findings to AWS Security Hub
@@ -101,7 +101,7 @@ prowler --security-hub --region eu-west-1
It is recommended to send only fails to Security Hub and that is possible adding `--status FAIL` to the command. You can use, instead of the `--status FAIL` argument, the `--send-sh-only-fails` argument to save all the findings in the Prowler outputs but just to send FAIL findings to AWS Security Hub.
-Since Prowler perform checks to all regions by default you may need to filter by region when running Security Hub integration, as shown in the example above. Remember to enable Security Hub in the region or regions you need by calling `aws securityhub enable-security-hub --region ` and run Prowler with the option `-f/--region ` (if no region is used it will try to push findings in all regions hubs). Prowler will send findings to the Security Hub on the region where the scanned resource is located.
+Since Prowler performs checks to all regions by default you may need to filter by region when running Security Hub integration, as shown in the example above. Remember to enable Security Hub in the region or regions you need by calling `aws securityhub enable-security-hub --region ` and run Prowler with the option `-f/--region ` (if no region is used it will try to push findings in all regions hubs). Prowler will send findings to the Security Hub on the region where the scanned resource is located.
To have updated findings in Security Hub you have to run Prowler periodically. Once a day or every certain amount of hours.
diff --git a/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx b/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx
index 7117ce1aae..7e6b3af272 100644
--- a/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx
+++ b/docs/user-guide/providers/aws/v2_to_v3_checks_mapping.mdx
@@ -20,7 +20,7 @@ checks_v4_v3_to_v2_mapping = {
"apigateway_restapi_public": "extra745",
"apigateway_restapi_logging_enabled": "extra722",
"apigateway_restapi_waf_acl_attached": "extra744",
- “apigatewayv2_api_access_logging_enabled": "extra7156",
+ "apigatewayv2_api_access_logging_enabled": "extra7156",
"apigatewayv2_api_authorizers_enabled": "extra7157",
"appstream_fleet_default_internet_access_disabled": "extra7193",
"appstream_fleet_maximum_session_duration": "extra7190",
diff --git a/docs/user-guide/providers/azure/authentication.mdx b/docs/user-guide/providers/azure/authentication.mdx
index 86d1ed6be9..ceea3a486f 100644
--- a/docs/user-guide/providers/azure/authentication.mdx
+++ b/docs/user-guide/providers/azure/authentication.mdx
@@ -3,9 +3,9 @@ title: 'Azure Authentication in Prowler'
description: 'Authenticate Prowler for Azure with a service principal, az CLI, browser login, or managed identity, and grant the required Entra and RBAC permissions.'
---
-Prowler for Azure supports multiple authentication types. Authentication methods vary between Prowler App and Prowler CLI:
+Prowler for Azure supports multiple authentication types. Authentication methods vary between Prowler Cloud and Prowler CLI:
-**Prowler App:**
+**Prowler Cloud:**
- [**Service Principal Application**](#service-principal-application-authentication-recommended)
@@ -146,7 +146,7 @@ The checks requiring this `ProwlerRole` can be found in this [section](/user-gui
1. To create a new custom role, open a terminal and execute the following command:
```console
- az role definition create --role-definition '{ 640ms lun 16 dic 17:04:17 2024
+ az role definition create --role-definition '{
"Name": "ProwlerRole",
"IsCustom": true,
"Description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
@@ -211,13 +211,15 @@ For more detailed guidance on subscription management and permissions:
The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks:
- `app_function_access_keys_configured`
+- `app_function_application_insights_enabled`
- `app_function_ftps_deployment_disabled`
+- `app_function_latest_runtime_version`
---
## Service Principal Application Authentication (Recommended)
-This method is required for Prowler App and recommended for Prowler CLI.
+This method is required for Prowler Cloud and recommended for Prowler CLI.
### Creating the Service Principal
For more information, see [Creating Prowler Service Principal](/user-guide/providers/azure/create-prowler-service-principal).
diff --git a/docs/user-guide/providers/azure/getting-started-azure.mdx b/docs/user-guide/providers/azure/getting-started-azure.mdx
index 1fecba506f..8519d4e179 100644
--- a/docs/user-guide/providers/azure/getting-started-azure.mdx
+++ b/docs/user-guide/providers/azure/getting-started-azure.mdx
@@ -17,7 +17,7 @@ Government cloud subscriptions (Azure Government) are not currently supported, b
### Prerequisites
-Before setting up Azure in Prowler App, you need to create a Service Principal with proper permissions.
+Before setting up Azure in Prowler Cloud, you need to create a Service Principal with proper permissions.
For detailed instructions on how to create the Service Principal and configure permissions, see [Authentication > Service Principal](/user-guide/providers/azure/authentication#service-principal-application-authentication-recommended).
@@ -35,12 +35,12 @@ For detailed instructions on how to create the Service Principal and configure p
### Step 2: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Navigate to `Configuration` > `Providers`

-3. Click on `Add Provider`
+3. Click `Add Provider`

@@ -54,14 +54,14 @@ For detailed instructions on how to create the Service Principal and configure p
### Step 3: Add Credentials to Prowler Cloud
-For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](/user-guide/providers/azure/authentication). When you finish creating and adding the [Entra](/user-guide/providers/azure/create-prowler-service-principal#assigning-proper-permissions) and [Subscription](/user-guide/providers/azure/subscriptions) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
+For Azure, Prowler Cloud uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](/user-guide/providers/azure/authentication). When you finish creating and adding the [Entra](/user-guide/providers/azure/create-prowler-service-principal#assigning-proper-permissions) and [Subscription](/user-guide/providers/azure/subscriptions) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
1. Go to your App Registration overview and copy the `Client ID` and `Tenant ID`

-2. Go to Prowler App and paste:
+2. Go to Prowler Cloud and paste:
- `Client ID`
- `Tenant ID`
diff --git a/docs/user-guide/providers/azure/subscriptions.mdx b/docs/user-guide/providers/azure/subscriptions.mdx
index 30134b2144..b05bcf263a 100644
--- a/docs/user-guide/providers/azure/subscriptions.mdx
+++ b/docs/user-guide/providers/azure/subscriptions.mdx
@@ -18,7 +18,7 @@ prowler azure --az-cli-auth --subscription-ids
-The multi-subscription feature is available only in the CLI. In Prowler App, each scan is limited to a single subscription.
+The multi-subscription feature is available only in the CLI. In Prowler Cloud, each scan is limited to a single subscription.
## Assigning Permissions for Subscription Scans
diff --git a/docs/user-guide/providers/cloudflare/authentication.mdx b/docs/user-guide/providers/cloudflare/authentication.mdx
index 2753b5f92b..66796224ae 100644
--- a/docs/user-guide/providers/cloudflare/authentication.mdx
+++ b/docs/user-guide/providers/cloudflare/authentication.mdx
@@ -57,8 +57,8 @@ Template URLs only pre-fill the token creation form. Review the permissions, con
### Step 1: Create a User API Token
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
-2. Click on the profile icon in the top right corner, then select "My Profile".
-3. Click on the **API Tokens** tab.
+2. Click the profile icon in the top right corner, then select "My Profile".
+3. Click the **API Tokens** tab.
4. Click **Create Token**, then select **Create Custom Token** at the bottom of the page.
5. Configure the token with the following settings:
- **Token name:** A descriptive name (e.g., "Prowler Security Scanner")
@@ -103,8 +103,8 @@ API Keys provide full access to the Cloudflare account. While supported, this me
### Step 1: Get the Global API Key
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
-2. Click on the profile icon in the top right corner, then select "My Profile".
-3. Click on the **API Tokens** tab.
+2. Click the profile icon in the top right corner, then select "My Profile".
+3. Click the **API Tokens** tab.
4. Scroll down to the **API Keys** section.
5. Click **View** next to **Global API Key**.
6. Enter the account password to reveal the key, then copy it.
diff --git a/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx b/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx
index 0e050bbd69..891480ef8d 100644
--- a/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx
+++ b/docs/user-guide/providers/cloudflare/getting-started-cloudflare.mdx
@@ -51,7 +51,7 @@ The Account ID is a 32-character hexadecimal string (e.g., `372e67954025e0ba6aaa
### Step 2: Open Prowler Cloud
-1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Providers".

diff --git a/docs/user-guide/providers/gcp/authentication.mdx b/docs/user-guide/providers/gcp/authentication.mdx
index fca9ddf943..89d2db0b35 100644
--- a/docs/user-guide/providers/gcp/authentication.mdx
+++ b/docs/user-guide/providers/gcp/authentication.mdx
@@ -56,7 +56,7 @@ This method uses the Google Cloud CLI to authenticate and is suitable for develo
### Setup Application Default Credentials
-1. In the [GCP Console](https://console.cloud.google.com/), click on "Activate Cloud Shell"
+1. In the [GCP Console](https://console.cloud.google.com/), click "Activate Cloud Shell"

@@ -90,7 +90,7 @@ This method uses the Google Cloud CLI to authenticate and is suitable for develo

-8. Extract the following values for Prowler Cloud/App:
+8. Extract the following values for Prowler Cloud or Prowler Local Server:
- `client_id`
- `client_secret`
diff --git a/docs/user-guide/providers/gcp/getting-started-gcp.mdx b/docs/user-guide/providers/gcp/getting-started-gcp.mdx
index 120eb9f7b9..a33fc8c9fa 100644
--- a/docs/user-guide/providers/gcp/getting-started-gcp.mdx
+++ b/docs/user-guide/providers/gcp/getting-started-gcp.mdx
@@ -14,7 +14,7 @@ description: 'Onboard a Google Cloud project to Prowler Cloud: retrieve the GCP
### Step 2: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Providers"

@@ -67,7 +67,7 @@ For Google Cloud, first enter your `GCP Project ID` and then select the authenti
2. Once authenticated, get the `Client ID`, `Client Secret` and `Refresh Token` from `~/.config/gcloud/application_default_credentials`.
- 3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into Prowler App.
+ 3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into Prowler Cloud.
diff --git a/docs/user-guide/providers/gcp/img/launch-scan.png b/docs/user-guide/providers/gcp/img/launch-scan.png
index 49511e5300..025da0a6a5 100644
Binary files a/docs/user-guide/providers/gcp/img/launch-scan.png and b/docs/user-guide/providers/gcp/img/launch-scan.png differ
diff --git a/docs/user-guide/providers/github/authentication.mdx b/docs/user-guide/providers/github/authentication.mdx
index ae6dc06ca8..4d591e26b3 100644
--- a/docs/user-guide/providers/github/authentication.mdx
+++ b/docs/user-guide/providers/github/authentication.mdx
@@ -13,7 +13,7 @@ Prowler offers three authentication methods. Fine-Grained Personal Access Tokens
| Method | Best For | Key Benefit |
|--------|----------|-------------|
-| [**Fine-Grained Personal Access Token**](#fine-grained-personal-access-token-recommended) | Individual users, quick setup | Simple, user-scoped access |
+| [**Fine-Grained Personal Access Token**](#fine-grained-personal-access-token-recommended-for-individual-use) | Individual users, quick setup | Simple, user-scoped access |
| [**GitHub App**](#github-app-credentials) | Organizations, automation, CI/CD | Organization-scoped, no personal account dependency |
| [**OAuth App Token**](#oauth-app-token) | Delegated user authorization | User-consented access flows |
@@ -272,7 +272,7 @@ Store the `.pem` private key securely. Anyone with this key can authenticate as
## Prowler Cloud Authentication
-For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp).
+For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud-and-prowler-local-server).
### Using Personal Access Token
@@ -302,7 +302,7 @@ For step-by-step setup instructions for Prowler Cloud, see the [Getting Started
3. Enter your GitHub App ID and upload the private key (`.pem` file).
-For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp).
+For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloud-and-prowler-local-server).
---
diff --git a/docs/user-guide/providers/github/getting-started-github.mdx b/docs/user-guide/providers/github/getting-started-github.mdx
index 5384e19592..ee5ce750a0 100644
--- a/docs/user-guide/providers/github/getting-started-github.mdx
+++ b/docs/user-guide/providers/github/getting-started-github.mdx
@@ -17,7 +17,7 @@ Prowler can scan either:
-
+
Web-based interface with centralized management
@@ -27,7 +27,7 @@ Prowler can scan either:
---
-## Prowler Cloud/App
+## Prowler Cloud and Prowler Local Server
@@ -35,7 +35,7 @@ Prowler can scan either:
### Prerequisites
-Before adding GitHub to Prowler Cloud/App, ensure you have:
+Before adding GitHub to Prowler Cloud or Prowler Local Server, ensure you have:
1. **GitHub Account Access**
- Personal GitHub account, OR
@@ -47,9 +47,9 @@ Before adding GitHub to Prowler Cloud/App, ensure you have:
- OAuth App Token
- GitHub App Credentials (Not Recommended - limited data access)
-### Step 1: Access Prowler Cloud/App
+### Step 1: Access Prowler Cloud or Prowler Local Server
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to **Configuration** → **Providers**

diff --git a/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx b/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx
index e13750a610..a9e9a30af0 100644
--- a/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx
+++ b/docs/user-guide/providers/googleworkspace/getting-started-googleworkspace.mdx
@@ -43,7 +43,7 @@ The Customer ID starts with the letter "C" followed by alphanumeric characters (
### Step 2: Open Prowler Cloud
-1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Providers".

diff --git a/docs/user-guide/providers/huaweicloud/authentication.mdx b/docs/user-guide/providers/huaweicloud/authentication.mdx
new file mode 100644
index 0000000000..d04da1620b
--- /dev/null
+++ b/docs/user-guide/providers/huaweicloud/authentication.mdx
@@ -0,0 +1,135 @@
+---
+title: "Huawei Cloud Authentication in Prowler"
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Prowler for Huawei Cloud authenticates against the Huawei Cloud APIs using an IAM user's **Access Key ID** and **Secret Access Key** (AK/SK). Credentials are read exclusively from environment variables to avoid exposing secrets in shell history or process listings; there are no credential CLI flags.
+
+## Required Credentials
+
+Prowler requires read access to the Huawei Cloud account. The following values are supported:
+
+| Credential | Environment Variable | Description |
+|------------|----------------------|-------------|
+| Access Key ID | `HUAWEICLOUD_ACCESS_KEY_ID` (or `HW_ACCESS_KEY`) | Permanent access key ID of the IAM user |
+| Secret Access Key | `HUAWEICLOUD_SECRET_ACCESS_KEY` (or `HW_SECRET_KEY`) | Secret access key paired with the access key ID |
+| Domain ID | `HUAWEICLOUD_DOMAIN_ID` (or `HW_DOMAIN_ID`) | Optional account (domain) ID |
+| Security Token | `HUAWEICLOUD_SECURITY_TOKEN` | Optional security token for temporary credentials |
+| Region | `HUAWEICLOUD_REGION` (or `HW_REGION`) | Default region(s) when `--region` is not passed (e.g. `eu-west-101`) |
+| Cloud | `HUAWEICLOUD_CLOUD` (or `HW_CLOUD`) | Scan every region of a cloud (`international`, `europe`, or `china`) when no region is set |
+
+
+The endpoint domain (`.eu` or `.com`) and the per-region project ID are resolved automatically from the region, so neither endpoint nor project configuration is needed. Multi-region scans work out of the box. For the region precedence rules and the full list of supported regions, see [Regions and Clouds](/user-guide/providers/huaweicloud/getting-started-huaweicloud#regions-and-clouds).
+
+
+
+Huawei Cloud runs separate clouds: **International** and **China** (`.com` endpoints) and **Huawei Cloud Europe** (`.eu` endpoints). The region (or `--cloud` selector) determines the endpoint, so accounts outside China must select a region they can reach — for example `eu-west-101` for a Huawei Cloud Europe account. A single set of credentials belongs to one cloud, so it cannot authenticate against both `.com` and `.eu`; scan each account with its own credentials.
+
+
+---
+
+## API Credentials
+
+### Step 1: Create an Access Key (AK/SK)
+
+1. Log in to the [Huawei Cloud console](https://console-intl.huaweicloud.com).
+2. Open **My Credentials** from the account menu, then select **Access Keys**.
+3. Click **Create Access Key** and complete the identity verification.
+4. Download the `credentials.csv` file — it contains the Access Key ID and Secret Access Key. The secret is not shown again.
+
+
+Use an IAM user with read-only permissions (for example, the built-in `ReadOnly` policy) rather than the account root credentials.
+
+
+### Step 2: Configure Authentication
+
+Export the credentials as environment variables:
+
+```bash
+export HUAWEICLOUD_ACCESS_KEY_ID="your-access-key-id"
+export HUAWEICLOUD_SECRET_ACCESS_KEY="your-secret-access-key"
+```
+
+Then run Prowler:
+
+```bash
+prowler huaweicloud
+```
+
+---
+
+## Assuming an Agency (Cross-Account)
+
+To scan a different account, assume an [agency](https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_06_0002.html) delegated to your account. Set the agency name and the target account, and Prowler exchanges the base credentials for temporary credentials scoped to the agency:
+
+```bash
+export HUAWEICLOUD_ACCESS_KEY_ID="your-access-key-id"
+export HUAWEICLOUD_SECRET_ACCESS_KEY="your-secret-access-key"
+export HUAWEICLOUD_AGENCY_NAME="your-agency-name"
+export HUAWEICLOUD_ASSUME_DOMAIN_ID="target-account-domain-id" # or HUAWEICLOUD_ASSUME_DOMAIN_NAME
+prowler huaweicloud
+```
+
+| Environment Variable | Description |
+|----------------------|-------------|
+| `HUAWEICLOUD_AGENCY_NAME` | Name of the agency to assume in the target account |
+| `HUAWEICLOUD_ASSUME_DOMAIN_ID` | Domain ID of the target (delegating) account |
+| `HUAWEICLOUD_ASSUME_DOMAIN_NAME` | Domain name of the target account (alternative to the domain ID) |
+
+---
+
+## Verifying Authentication
+
+To confirm that Prowler can reach the account, run a scan against a single region the account can reach:
+
+```bash
+# China account
+prowler huaweicloud --region cn-north-4
+
+# International account
+prowler huaweicloud --region ap-southeast-1
+
+# Huawei Cloud Europe account
+prowler huaweicloud --region eu-west-101
+```
+
+To scan the account's entire cloud instead, use the `--cloud` selector:
+
+```bash
+prowler huaweicloud --cloud europe
+```
+
+A successful run reports findings for the discovered resources. A failed run displays an error message indicating the credential or connectivity issue.
+
+---
+
+## CI/CD Integration
+
+For automated pipelines, set the credentials as secret environment variables:
+
+**GitHub Actions:**
+
+```yaml
+env:
+ HUAWEICLOUD_ACCESS_KEY_ID: ${{ secrets.HUAWEICLOUD_ACCESS_KEY_ID }}
+ HUAWEICLOUD_SECRET_ACCESS_KEY: ${{ secrets.HUAWEICLOUD_SECRET_ACCESS_KEY }}
+
+steps:
+ - name: Run Prowler
+ run: prowler huaweicloud
+```
+
+**GitLab CI:**
+
+```yaml
+variables:
+ HUAWEICLOUD_ACCESS_KEY_ID: $HUAWEICLOUD_ACCESS_KEY_ID
+ HUAWEICLOUD_SECRET_ACCESS_KEY: $HUAWEICLOUD_SECRET_ACCESS_KEY
+
+prowler_scan:
+ script:
+ - prowler huaweicloud
+```
diff --git a/docs/user-guide/providers/huaweicloud/getting-started-huaweicloud.mdx b/docs/user-guide/providers/huaweicloud/getting-started-huaweicloud.mdx
new file mode 100644
index 0000000000..f6ccd1024c
--- /dev/null
+++ b/docs/user-guide/providers/huaweicloud/getting-started-huaweicloud.mdx
@@ -0,0 +1,177 @@
+---
+title: 'Getting Started With Huawei Cloud on Prowler'
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Prowler for Huawei Cloud scans your Huawei Cloud account for security misconfigurations across compute, storage, networking, identity, encryption, database, and logging services.
+
+
+Huawei Cloud support in Prowler is community-maintained. For commercial support or to request additional service coverage, [contact us](https://prowler.com/contact).
+
+
+## Prerequisites
+
+Set up authentication for Huawei Cloud with the [Huawei Cloud Authentication](/user-guide/providers/huaweicloud/authentication) guide before starting:
+
+- Create an Access Key ID and Secret Access Key (AK/SK) for an IAM user with read-only permissions.
+- Prowler reads the credentials exclusively from environment variables, so secrets are never passed on the command line.
+
+## Prowler CLI
+
+### Run Prowler for Huawei Cloud
+
+Once authenticated, export the credentials as environment variables and run Prowler for Huawei Cloud. Environment variables keep secrets out of shell history and process listings:
+
+```bash
+export HUAWEICLOUD_ACCESS_KEY_ID="your-access-key-id"
+export HUAWEICLOUD_SECRET_ACCESS_KEY="your-secret-access-key"
+prowler huaweicloud
+```
+
+### Run Specific Checks
+
+```bash
+prowler huaweicloud --checks obs_bucket_public_access iam_user_mfa_enabled
+```
+
+### Run a Specific Service
+
+```bash
+prowler huaweicloud --services iam
+```
+
+## Regions and Clouds
+
+Huawei Cloud operates as three separate clouds, and every account belongs to exactly one of them:
+
+- **International** — served from the `.com` endpoints (for example `myhuaweicloud.com`).
+- **China** — also served from the `.com` endpoints, on the China regions (`cn-*`).
+- **Huawei Cloud Europe** — served from the `.eu` endpoints (for example `myhuaweicloud.eu`).
+
+The cloud is a property of the region: each region ID maps to exactly one endpoint domain. Prowler selects the correct endpoint automatically from the region, so no endpoint configuration is required.
+
+### Region Selection Precedence
+
+Prowler resolves the regions to scan from the first source that is set, in this order:
+
+1. **`--region` flag** (aliases `--filter-region`, `-f`) — one or more explicit region IDs.
+2. **`HUAWEICLOUD_REGION`** (or `HW_REGION`) environment variable — one or more region IDs, separated by spaces or commas.
+3. **`--cloud` selector** (or `HUAWEICLOUD_CLOUD` / `HW_CLOUD`) — expands to every region of the selected cloud.
+4. **Default** — when none is set, Prowler falls back to its built-in region list.
+
+A more specific source always wins: `--region` overrides `HUAWEICLOUD_REGION`, which overrides `--cloud`.
+
+### Select Specific Regions
+
+To scan a defined set of regions, pass the region IDs to `--region` or set `HUAWEICLOUD_REGION`. Accounts outside China must select a region they can reach — for example `eu-west-101` for Huawei Cloud Europe or `ap-southeast-1` for International.
+
+```bash
+# Flag (one or more regions)
+prowler huaweicloud --region eu-west-101 ap-southeast-1
+
+# Environment variable (space- or comma-separated)
+export HUAWEICLOUD_REGION="ap-southeast-1, ap-southeast-2"
+prowler huaweicloud
+```
+
+### Scan an Entire Cloud
+
+To scan every region of an account's cloud without listing regions, use the `--cloud` selector or the `HUAWEICLOUD_CLOUD` environment variable. Prowler expands it to that cloud's regions and selects the matching endpoint automatically:
+
+```bash
+# Scan all Huawei Cloud Europe regions (.eu endpoints)
+prowler huaweicloud --cloud europe
+
+# Scan all International regions (.com endpoints)
+prowler huaweicloud --cloud international
+
+# Scan all China regions (.com endpoints)
+prowler huaweicloud --cloud china
+```
+
+The `--cloud` flag accepts three values: `international`, `europe`, and `china`. The `HUAWEICLOUD_CLOUD` (or `HW_CLOUD`) environment variable additionally accepts the short aliases `intl`/`com` (International), `eu` (Europe), and `cn` (China).
+
+
+A single set of credentials belongs to one cloud, so `--cloud` selects which cloud to scan — it cannot authenticate against both `.com` and `.eu` at once. To scan accounts on different clouds, run Prowler once per account with that account's credentials.
+
+
+### How Prowler Handles Regions and Endpoints
+
+Prowler manages several Huawei Cloud specifics automatically during a scan:
+
+- **Endpoint selection:** The endpoint domain (`.eu` or `.com`) is derived from each region, so every service targets the right cloud. This corrects services whose bundled metadata still points Europe regions at `.com`.
+- **Project resolution:** The per-region project ID is resolved automatically, so multi-region scans work without any project configuration.
+- **Credential validation:** Credentials are validated against a region in the account's cloud that exposes IAM, so validation succeeds even when the requested regions do not all offer IAM.
+- **Unsupported regions:** Any region a given service does not offer is skipped and logged, so scanning an entire cloud never fails on regions where a service is unavailable.
+
+### Supported Regions
+
+Prowler recognizes the following region IDs, grouped by cloud.
+
+**International (`.com`)**
+
+| Region ID | Location |
+|-----------|----------|
+| `ae-ad-1` | UAE (Abu Dhabi) |
+| `af-north-1` | Egypt (Cairo) |
+| `af-south-1` | South Africa |
+| `ap-southeast-1` | Hong Kong |
+| `ap-southeast-2` | Singapore |
+| `ap-southeast-3` | Thailand |
+| `ap-southeast-4` | Malaysia |
+| `ap-southeast-5` | Indonesia (Jakarta) |
+| `eu-west-0` | Ireland |
+| `la-north-2` | Mexico |
+| `la-south-2` | Chile (Santiago) |
+| `me-east-1` | UAE (Dubai) |
+| `my-kualalumpur-1` | Malaysia (Kuala Lumpur) |
+| `na-mexico-1` | Mexico (Mexico City) |
+| `ru-moscow-1` | Russia (Moscow-1) |
+| `sa-brazil-1` | Brazil |
+| `tr-west-1` | Türkiye (Istanbul) |
+
+**Huawei Cloud Europe (`.eu`)**
+
+| Region ID | Location |
+|-----------|----------|
+| `eu-west-101` | Ireland (Dublin) |
+
+**China (`.com`)**
+
+| Region ID | Location |
+|-----------|----------|
+| `cn-north-1` | China (Beijing-1) |
+| `cn-north-2` | China (Beijing-2) |
+| `cn-north-4` | China (Beijing-4) |
+| `cn-north-9` | China (Ulanqab) |
+| `cn-north-11` | China (Ulanqab-11) |
+| `cn-north-12` | China (Ulanqab-12) |
+| `cn-east-2` | China (Shanghai-2) |
+| `cn-east-3` | China (Shanghai-1) |
+| `cn-east-4` | China (Shanghai-4) |
+| `cn-east-5` | China (Shanghai-5) |
+| `cn-south-1` | China (Guangzhou) |
+| `cn-south-2` | China (Guangzhou-2) |
+| `cn-south-4` | China (Guangzhou-4) |
+| `cn-southwest-2` | China (Guiyang) |
+| `cn-southwest-3` | China (Guiyang-3) |
+
+## Available Services
+
+Prowler for Huawei Cloud currently supports the following services:
+
+| Service | Description |
+|---------|-------------|
+| `cts` | Cloud Trace Service trackers that record account and API activity for audit logging |
+| `ecs` | Elastic Cloud Server compute instances and their key pair, public IP, and security group configuration |
+| `elb` | Elastic Load Balance load balancers and their public exposure |
+| `evs` | Elastic Volume Service block volumes and their encryption settings |
+| `iam` | Identity and Access Management users, MFA devices, password policy, and account operation protection |
+| `kms` | Key Management Service keys, their state, and rotation configuration |
+| `obs` | Object Storage Service buckets and their public-access configuration |
+| `rds` | Relational Database Service instances and their public access, backup, and disk-encryption settings |
+| `vpc` | Virtual Private Cloud security groups and their ingress rules |
+| `waf` | Web Application Firewall instances and their status |
diff --git a/docs/user-guide/providers/iac/getting-started-iac.mdx b/docs/user-guide/providers/iac/getting-started-iac.mdx
index 18cbd6025e..7f504dea95 100644
--- a/docs/user-guide/providers/iac/getting-started-iac.mdx
+++ b/docs/user-guide/providers/iac/getting-started-iac.mdx
@@ -25,7 +25,7 @@ Prowler IaC provider scans the following Infrastructure as Code configurations f
## How It Works
-- Prowler App leverages [Trivy](https://trivy.dev/docs/latest/guide/coverage/iac/#scanner) to scan local directories (or specified paths) for supported IaC files, or scans remote repositories.
+- Prowler Cloud leverages [Trivy](https://trivy.dev/docs/latest/guide/coverage/iac/#scanner) to scan local directories (or specified paths) for supported IaC files, or scans remote repositories.
- No cloud credentials or authentication are required for local scans.
- For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables.
- Check the [IaC Authentication](/user-guide/providers/iac/authentication) page for more details.
@@ -38,11 +38,11 @@ Prowler IaC provider scans the following Infrastructure as Code configurations f
### Supported Scanners
-Scanner selection is not configurable in Prowler App. Default scanners, misconfig and secret, run automatically during each scan.
+Scanner selection is not configurable in Prowler Cloud. Default scanners, misconfig and secret, run automatically during each scan.
-### Step 1: Access Prowler Cloud/App
+### Step 1: Access Prowler Cloud or Prowler Local Server
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Providers"

diff --git a/docs/user-guide/providers/image/getting-started-image.mdx b/docs/user-guide/providers/image/getting-started-image.mdx
index 6323c872fc..b94a394c7b 100644
--- a/docs/user-guide/providers/image/getting-started-image.mdx
+++ b/docs/user-guide/providers/image/getting-started-image.mdx
@@ -34,7 +34,7 @@ Prowler Cloud does not support scanner selection. The vulnerability, secret, and
### Step 1: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Navigate to "Configuration" > "Providers"

diff --git a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx
index e0c155c59b..737f585e3f 100644
--- a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx
+++ b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx
@@ -5,9 +5,9 @@ description: 'Onboard Kubernetes clusters to Prowler and scan for security misco
## Prowler Cloud
-### Step 1: Access Prowler Cloud/App
+### Step 1: Access Prowler Cloud or Prowler Local Server
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Providers"

@@ -22,10 +22,10 @@ description: 'Onboard Kubernetes clusters to Prowler and scan for security misco
### Step 2: Configure Kubernetes Authentication
-For Kubernetes, Prowler App uses a `kubeconfig` file to authenticate. Paste the contents of your `kubeconfig` file into the `Kubeconfig content` field.
+For Kubernetes, Prowler Cloud uses a `kubeconfig` file to authenticate. Paste the contents of your `kubeconfig` file into the `Kubeconfig content` field.
-Kubeconfigs that use `users[].user.exec` authentication are not supported in Prowler Cloud/App. For security reasons, Prowler Cloud does not run commands declared by uploaded kubeconfigs. Use kubeconfig credentials that do not rely on `exec` authentication, such as the ServiceAccount token flow documented below.
+Kubeconfigs that use `users[].user.exec` authentication are not supported in Prowler Cloud or Prowler Local Server. For security reasons, Prowler Cloud does not run commands declared by uploaded kubeconfigs. Use kubeconfig credentials that do not rely on `exec` authentication, such as the ServiceAccount token flow documented below.
By default, the `kubeconfig` file is located at `~/.kube/config`.
diff --git a/docs/user-guide/providers/linode/authentication.mdx b/docs/user-guide/providers/linode/authentication.mdx
index 7f051f18d6..00afc4637a 100644
--- a/docs/user-guide/providers/linode/authentication.mdx
+++ b/docs/user-guide/providers/linode/authentication.mdx
@@ -30,7 +30,7 @@ Ensure the token has all required scopes. Missing permissions will cause some ch
### 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.
+2. Click 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")
diff --git a/docs/user-guide/providers/microsoft365/authentication.mdx b/docs/user-guide/providers/microsoft365/authentication.mdx
index 2841d8236d..c735f80b3b 100644
--- a/docs/user-guide/providers/microsoft365/authentication.mdx
+++ b/docs/user-guide/providers/microsoft365/authentication.mdx
@@ -552,7 +552,7 @@ Installing PowerShell is different depending on your OS:
- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell:
+ [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command downloads the latest stable versions of PowerShell:
```console
docker pull mcr.microsoft.com/dotnet/sdk:9.0
diff --git a/docs/user-guide/providers/microsoft365/getting-started-m365.mdx b/docs/user-guide/providers/microsoft365/getting-started-m365.mdx
index 05a02313ed..0afe58e438 100644
--- a/docs/user-guide/providers/microsoft365/getting-started-m365.mdx
+++ b/docs/user-guide/providers/microsoft365/getting-started-m365.mdx
@@ -42,7 +42,7 @@ Set up authentication for Microsoft 365 with the [Microsoft 365 Authentication](
### Step 2: Open Prowler Cloud
-1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Providers".

diff --git a/docs/user-guide/providers/microsoft365/use-of-powershell.mdx b/docs/user-guide/providers/microsoft365/use-of-powershell.mdx
index b5d08467be..5de97cb2bf 100644
--- a/docs/user-guide/providers/microsoft365/use-of-powershell.mdx
+++ b/docs/user-guide/providers/microsoft365/use-of-powershell.mdx
@@ -14,4 +14,4 @@ To learn more about how to install PowerShell and which versions are supported,
## Required Modules
The necessary modules will not be installed automatically by Prowler. Nevertheless, if you want Prowler to install them for you, you can execute the provider with the flag `--init-modules`, which will run the script to install and import them.
-If you want to learn more about this process or you are running some issues with this, click [here](/user-guide/providers/microsoft365/authentication#required-powershell-modules).
+If you want to learn more about this process or you are running into some issues with this, click [here](/user-guide/providers/microsoft365/authentication#required-powershell-modules).
diff --git a/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx b/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx
index 601ab9f298..e6c89023ff 100644
--- a/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx
+++ b/docs/user-guide/providers/mongodbatlas/getting-started-mongodbatlas.mdx
@@ -16,12 +16,12 @@ Before you begin, make sure you have:
3. An **API Key pair** (public and private keys) with appropriate permissions:
- **Organization Read Only**: Provides read-only access to everything in the organization, including all projects in the organization. This permission is sufficient for most security checks.
- **Organization Owner**: Required to audit the [Auditing configuration](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-auditing) for projects. Database auditing tracks database operations and security events, including authentication attempts, data definition language (DDL) changes, user and role modifications, and privilege grants. This configuration is essential for security monitoring, forensics, and compliance. Without **Organization Owner** permission, the `projects_auditing_enabled` check cannot retrieve the audit configuration status.
-4. Prowler App access (cloud or self-hosted) or the Prowler CLI (`pip install prowler`).
+4. Access to Prowler Cloud or Prowler Local Server, or Prowler CLI (`pip install prowler`).
For detailed instructions on creating API keys, see the [MongoDB Atlas authentication guide](./authentication.mdx).
-If **Require IP Access List for the Atlas Administration API** is enabled in your organization settings, you **must** add the IP address of the host running Prowler (or the public IP of Prowler Cloud) to the organization IP Access List or Atlas will reject every API call. You can manage this under **Settings → Organization Settings → Security**. See step 7 of the [authentication guide](./authentication.mdx) for detailed instructions, and refer to the [Prowler Cloud public IP list](../../tutorials/prowler-cloud-public-ips) when using Prowler Cloud.
+If **Require IP Access List for the Atlas Administration API** is enabled in the organization settings, add the IP address of the host running Prowler (or the public IP of Prowler Cloud) to the organization IP Access List or Atlas will reject every API call. Manage this under **Settings → Organization Settings → Security**. See step 7 of the [authentication guide](./authentication.mdx) for detailed instructions, and refer to the [Prowler Cloud egress IPs](/security/networking) when using Prowler Cloud.
@@ -54,7 +54,7 @@ If **Require IP Access List for the Atlas Administration API** is enabled in you
### Step 3: Test the connection and start scanning
-1. Click **Test connection** to ensure Prowler App can reach the Atlas API.
+1. Click **Test connection** to ensure Prowler Cloud can reach the Atlas API.
2. Save the credentials. The provider will appear in the list with its current connection status.
3. Launch a scan from the provider row or from the **Scans** page.

@@ -109,6 +109,6 @@ prowler mongodbatlas --atlas-project-id
- Combine flags (for example, `--checks` or `--services`) just like with other providers.
- Use `--output-modes` to export findings in JSON, CSV, ASFF, etc.
-- Rotate API keys regularly and update the stored credentials in Prowler App to maintain connectivity.
+- Rotate API keys regularly and update the stored credentials in Prowler Cloud to maintain connectivity.
For more examples (filters, outputs, scheduling), refer back to the [MongoDB Atlas documentation hub](./authentication.mdx) and the main Prowler CLI usage guide.
diff --git a/docs/user-guide/providers/mongodbatlas/img/add-credentials.png b/docs/user-guide/providers/mongodbatlas/img/add-credentials.png
index 646a8e9422..9db07b5dc4 100644
Binary files a/docs/user-guide/providers/mongodbatlas/img/add-credentials.png and b/docs/user-guide/providers/mongodbatlas/img/add-credentials.png differ
diff --git a/docs/user-guide/providers/mongodbatlas/img/add-org-id.png b/docs/user-guide/providers/mongodbatlas/img/add-org-id.png
index b8306e95b0..11b251e9a1 100644
Binary files a/docs/user-guide/providers/mongodbatlas/img/add-org-id.png and b/docs/user-guide/providers/mongodbatlas/img/add-org-id.png differ
diff --git a/docs/user-guide/providers/mongodbatlas/img/add-provider-list.png b/docs/user-guide/providers/mongodbatlas/img/add-provider-list.png
index 72e8341227..cbcb487ebb 100644
Binary files a/docs/user-guide/providers/mongodbatlas/img/add-provider-list.png and b/docs/user-guide/providers/mongodbatlas/img/add-provider-list.png differ
diff --git a/docs/user-guide/providers/oci/authentication.mdx b/docs/user-guide/providers/oci/authentication.mdx
index 1237ae8b38..84970675be 100644
--- a/docs/user-guide/providers/oci/authentication.mdx
+++ b/docs/user-guide/providers/oci/authentication.mdx
@@ -56,7 +56,7 @@ After running `oci session authenticate`, you need to manually add your user OCI
**Get your user OCID from the OCI Console:**
-Navigate to: **Identity & Security** → **Users** → Click on your username → Copy the OCID
+Navigate to: **Identity & Security** → **Users** → Click your username → Copy the OCID

@@ -435,7 +435,7 @@ prowler oci --oci-config-file /path/to/config
**Cause**: Insufficient IAM permissions
-**Solution**: Add required policies (see [Required Permissions](./getting-started-oci.md#required-permissions))
+**Solution**: Add required policies (see [Required Permissions](/user-guide/providers/oci/getting-started-oci#required-permissions))
### Configuration Validation
diff --git a/docs/user-guide/providers/oci/getting-started-oci.mdx b/docs/user-guide/providers/oci/getting-started-oci.mdx
index 8b44c0350a..9b2c69eb5a 100644
--- a/docs/user-guide/providers/oci/getting-started-oci.mdx
+++ b/docs/user-guide/providers/oci/getting-started-oci.mdx
@@ -7,7 +7,7 @@ Prowler supports security scanning of Oracle Cloud Infrastructure (OCI) environm
## Prowler Cloud
-The following steps apply to Prowler Cloud and the self-hosted Prowler App.
+The following steps apply to Prowler Cloud and Prowler Local Server.
### Step 1: Collect OCI Identifiers
1. Sign in to the [OCI Console](https://cloud.oracle.com/) and open **Tenancy Details** to copy the Tenancy OCID.
@@ -16,14 +16,14 @@ The following steps apply to Prowler Cloud and the self-hosted Prowler App.
4. Note the **Region** identifier to scan (for example, `us-ashburn-1`).
### Step 2: Access Prowler Cloud
-1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Go to **Configuration** → **Providers** and click **Add Provider**.

3. Select **Oracle Cloud** and enter the **Tenancy OCID** and an optional alias, then choose **Next**.

### Step 3: Add OCI API Key Credentials
-Prowler App connects to OCI with API key credentials. Provide:
+Prowler Cloud connects to OCI with API key credentials. Provide:
- **User OCID** for the API key owner
- **Fingerprint** of the API key
@@ -59,7 +59,7 @@ Before you begin, ensure you have:
### Authentication
-Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](./authentication).
+Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](/user-guide/providers/oci/authentication).
**Note:** OCI Session Authentication and Config File Authentication both use the same `~/.oci/config` file. The difference is how the config file is generated - automatically via browser (session auth) or manually with API keys.
@@ -79,7 +79,7 @@ The easiest and most secure method is using OCI session authentication, which au
**Get your user OCID from the OCI Console:**
- Navigate to: **Identity & Security** → **Users** → Click on your username → Copy the OCID
+ Navigate to: **Identity & Security** → **Users** → Click your username → Copy the OCID

@@ -108,7 +108,7 @@ The easiest and most secure method is using OCI session authentication, which au
#### Alternative: Manual API Key Setup
-If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](./authentication#config-file-authentication-manual-api-key-setup).
+If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](/user-guide/providers/oci/authentication#config-file-authentication-manual-api-key-setup).
**Note:** Both methods use the same `~/.oci/config` file - the difference is that manual setup uses static API keys while session authentication uses temporary session tokens.
diff --git a/docs/user-guide/providers/oci/images/oci-add-api-key-credentials.png b/docs/user-guide/providers/oci/images/oci-add-api-key-credentials.png
index 886b6ee876..38bf82f371 100644
Binary files a/docs/user-guide/providers/oci/images/oci-add-api-key-credentials.png and b/docs/user-guide/providers/oci/images/oci-add-api-key-credentials.png differ
diff --git a/docs/user-guide/providers/oci/images/oci-add-cloud-provider.png b/docs/user-guide/providers/oci/images/oci-add-cloud-provider.png
index f4757afad6..d8860df172 100644
Binary files a/docs/user-guide/providers/oci/images/oci-add-cloud-provider.png and b/docs/user-guide/providers/oci/images/oci-add-cloud-provider.png differ
diff --git a/docs/user-guide/providers/oci/images/oci-add-tenancy.png b/docs/user-guide/providers/oci/images/oci-add-tenancy.png
index a671272034..10dfe0f1a2 100644
Binary files a/docs/user-guide/providers/oci/images/oci-add-tenancy.png and b/docs/user-guide/providers/oci/images/oci-add-tenancy.png differ
diff --git a/docs/user-guide/providers/okta/authentication.mdx b/docs/user-guide/providers/okta/authentication.mdx
index 063403001d..baba932e13 100644
--- a/docs/user-guide/providers/okta/authentication.mdx
+++ b/docs/user-guide/providers/okta/authentication.mdx
@@ -21,7 +21,7 @@ Prowler authenticates to Okta as a **service application** using **OAuth 2.0 wit
| Method | Status | Use Case |
|---|---|---|
-| **OAuth 2.0 (private-key JWT)** | Supported | Production scans, CI/CD, Prowler App. |
+| **OAuth 2.0 (private-key JWT)** | Supported | Production scans, CI/CD, Prowler Cloud. |
The private-key JWT flow is the only supported authentication method in the initial release. The service application proves possession of a private key on every token request; Okta returns a short-lived access token, refreshed automatically by the SDK.
diff --git a/docs/user-guide/providers/okta/getting-started-okta.mdx b/docs/user-guide/providers/okta/getting-started-okta.mdx
index a0b66bcb60..c1ba449e36 100644
--- a/docs/user-guide/providers/okta/getting-started-okta.mdx
+++ b/docs/user-guide/providers/okta/getting-started-okta.mdx
@@ -31,7 +31,7 @@ Set up authentication for Okta with the [Okta Authentication](/user-guide/provid
### Step 1: Add the Provider
-1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Providers".

diff --git a/docs/user-guide/providers/okta/images/okta-credentials-form.png b/docs/user-guide/providers/okta/images/okta-credentials-form.png
index c37553c59d..20843b6555 100644
Binary files a/docs/user-guide/providers/okta/images/okta-credentials-form.png and b/docs/user-guide/providers/okta/images/okta-credentials-form.png differ
diff --git a/docs/user-guide/providers/okta/images/okta-org-domain-form.png b/docs/user-guide/providers/okta/images/okta-org-domain-form.png
index fb145d2877..a43f36b57d 100644
Binary files a/docs/user-guide/providers/okta/images/okta-org-domain-form.png and b/docs/user-guide/providers/okta/images/okta-org-domain-form.png differ
diff --git a/docs/user-guide/providers/okta/images/select-okta-provider.png b/docs/user-guide/providers/okta/images/select-okta-provider.png
index 1a854bab37..23b869c124 100644
Binary files a/docs/user-guide/providers/okta/images/select-okta-provider.png and b/docs/user-guide/providers/okta/images/select-okta-provider.png differ
diff --git a/docs/user-guide/providers/openstack/images/add-credentials.png b/docs/user-guide/providers/openstack/images/add-credentials.png
index c2583e5358..2f317fad22 100644
Binary files a/docs/user-guide/providers/openstack/images/add-credentials.png and b/docs/user-guide/providers/openstack/images/add-credentials.png differ
diff --git a/docs/user-guide/providers/openstack/images/add-provider-id.png b/docs/user-guide/providers/openstack/images/add-provider-id.png
index 75266903fd..a2e4e900ef 100644
Binary files a/docs/user-guide/providers/openstack/images/add-provider-id.png and b/docs/user-guide/providers/openstack/images/add-provider-id.png differ
diff --git a/docs/user-guide/providers/openstack/images/select-provider.png b/docs/user-guide/providers/openstack/images/select-provider.png
index 3a280848c4..fcc362c12b 100644
Binary files a/docs/user-guide/providers/openstack/images/select-provider.png and b/docs/user-guide/providers/openstack/images/select-provider.png differ
diff --git a/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx b/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx
index 71a9815024..f67a97fb95 100644
--- a/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx
+++ b/docs/user-guide/providers/scaleway/getting-started-scaleway.mdx
@@ -15,7 +15,7 @@ Prowler for Scaleway scans IAM resources in your Scaleway organization for secur
Prowler authenticates to Scaleway with a Scaleway API key. See [Scaleway Authentication in Prowler](./authentication) for the full setup, environment variables, CLI flags, and required permissions.
-## Run a scan
+## Run a Scan
```bash
export SCW_ACCESS_KEY="SCW..."
@@ -31,7 +31,7 @@ To run only the IAM root-key check:
prowler scaleway --check iam_api_keys_no_root_owned
```
-## Checks shipped
+## Checks Shipped
| Check ID | Severity | Description |
|---|---|---|
diff --git a/docs/user-guide/providers/vercel/getting-started-vercel.mdx b/docs/user-guide/providers/vercel/getting-started-vercel.mdx
index b0f19c6432..08495c4b4a 100644
--- a/docs/user-guide/providers/vercel/getting-started-vercel.mdx
+++ b/docs/user-guide/providers/vercel/getting-started-vercel.mdx
@@ -29,7 +29,7 @@ Set up authentication for Vercel with the [Vercel Authentication](/user-guide/pr
### Step 1: Add the Provider
-1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
+1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Providers".

diff --git a/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx b/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx
index 1f329c43c0..15c77daefd 100644
--- a/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx
+++ b/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx
@@ -10,9 +10,9 @@ The tool, `aws_org_generator.py`, complements the [Bulk Provider Provisioning
**Native AWS Organizations support is now available in Prowler Cloud.** You can onboard all accounts via the UI wizard — with automatic discovery, hierarchical tree selection, connection testing, and bulk scan launch — without any scripts or YAML files.
-See [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations).
+See [AWS Organizations](/user-guide/tutorials/prowler-cloud-aws-organizations) in Prowler Cloud.
-The CLI-based tool below remains useful for self-hosted Prowler App and advanced automation scenarios.
+The CLI-based tool below remains useful for Prowler Local Server and advanced automation scenarios.
{/* TODO: Add screenshot of the tool in action */}
@@ -33,9 +33,9 @@ The AWS Organizations Bulk Provisioning tool simplifies multi-account onboarding
* Python 3.7 or higher
* AWS credentials with Organizations read access
* ProwlerRole (or custom role) deployed across all target accounts
-* Prowler API key (from Prowler Cloud or self-hosted Prowler App)
- * For self-hosted Prowler App, remember to [point to your API base URL](./bulk-provider-provisioning#custom-api-endpoints)
- * Learn how to create API keys: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
+* Prowler API key (from Prowler Cloud or Prowler Local Server)
+ * For Prowler Local Server, remember to [point to your API base URL](./bulk-provider-provisioning#custom-api-endpoints)
+ * Learn how to create API keys: [Prowler Cloud API Keys](../tutorials/prowler-app-api-keys)
### Deploying ProwlerRole Across AWS Organizations
@@ -98,13 +98,13 @@ export PROWLER_API_KEY="pk_example-api-key"
To create an API key:
-1. Log in to Prowler Cloud or Prowler App
+1. Log in to Prowler Cloud or Prowler Local Server
2. Click **Profile** → **Account**
3. Click **Create API Key**
4. Provide a descriptive name and optionally set an expiration date
5. Copy the generated API key (it will only be shown once)
-For detailed instructions, see: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
+For detailed instructions, see: [Prowler Cloud API Keys](../tutorials/prowler-app-api-keys)
## Basic Usage
@@ -273,6 +273,8 @@ python aws_org_generator.py \
4. Deploy to all organizational units
5. Use a unique external ID (e.g., `prowler-org-2024-abc123`)
+ Alternatively, deploy the same template as a **single stack** with `DeployStackSet=true` and `AWSOrganizationalUnitId` set to your root/OU ID — it creates the StackSet for you. See [Native CloudFormation StackSet Deployment](../providers/aws/organizations#native-cloudformation-stackset-deployment-recommended).
+
{/* TODO: Add screenshot of CloudFormation StackSets deployment */}
@@ -325,7 +327,7 @@ python aws_org_generator.py \
- Provision all accounts to Prowler Cloud or Prowler App:
+ Provision all accounts to Prowler Cloud or Prowler Local Server:
```bash
# Set Prowler API key
@@ -488,7 +490,7 @@ grep "provider: aws" aws-org-accounts.yaml | wc -l
Learn how to bulk provision providers in Prowler.
-
+
Detailed instructions on how to use Prowler.
diff --git a/docs/user-guide/tutorials/bulk-provider-provisioning.mdx b/docs/user-guide/tutorials/bulk-provider-provisioning.mdx
index 1f25a5f77e..b38022611d 100644
--- a/docs/user-guide/tutorials/bulk-provider-provisioning.mdx
+++ b/docs/user-guide/tutorials/bulk-provider-provisioning.mdx
@@ -11,7 +11,7 @@ The tool is available in the Prowler repository at: [util/prowler-bulk-provision
## Overview
-The Bulk Provider Provisioning tool automates the creation of cloud providers in Prowler App or Prowler Cloud by:
+The Bulk Provider Provisioning tool automates the creation of cloud providers in Prowler Cloud or Prowler Local Server by:
* Reading provider configurations from YAML files
* Creating providers with appropriate authentication credentials
@@ -27,9 +27,9 @@ The Bulk Provider Provisioning tool automates the creation of cloud providers in
### Requirements
* Python 3.7 or higher
-* Prowler API key (from Prowler Cloud or self-hosted Prowler App)
- * For self-hosted Prowler App, remember to [point to your API base URL](#custom-api-endpoints)
- * Learn how to create API keys: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
+* Prowler API key (from Prowler Cloud or Prowler Local Server)
+ * For Prowler Local Server, remember to [point to your API base URL](#custom-api-endpoints)
+ * Learn how to create API keys: [Prowler Cloud API Keys](../tutorials/prowler-app-api-keys)
* Authentication credentials for target cloud providers
### Installation
@@ -52,13 +52,13 @@ export PROWLER_API_KEY="pk_example-api-key"
To create an API key:
-1. Log in to Prowler Cloud or Prowler App
+1. Log in to Prowler Cloud or Prowler Local Server
2. Click **Profile** → **Account**
3. Click **Create API Key**
4. Provide a descriptive name and optionally set an expiration date
5. Copy the generated API key (it will only be shown once)
-For detailed instructions, see: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
+For detailed instructions, see: [Prowler Cloud API Keys](../tutorials/prowler-app-api-keys)
## Configuration File Structure
@@ -262,7 +262,7 @@ python prowler_bulk_provisioning.py providers.yaml --concurrency 10
### Custom API Endpoints
-For self-hosted Prowler App installations:
+For Prowler Local Server installations:
```bash
python prowler_bulk_provisioning.py providers.yaml \
diff --git a/docs/user-guide/tutorials/prowler-app-api-keys.mdx b/docs/user-guide/tutorials/prowler-app-api-keys.mdx
index d915d476fb..ea9c8c8d9b 100644
--- a/docs/user-guide/tutorials/prowler-app-api-keys.mdx
+++ b/docs/user-guide/tutorials/prowler-app-api-keys.mdx
@@ -4,14 +4,17 @@ description: 'Create, manage, and revoke Prowler App API keys for automation, CI
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-API key authentication in Prowler App provides an alternative to JWT tokens and empowers automation, CI/CD pipelines, and third-party integrations. This guide explains how to create, manage, and safeguard API keys when working with the Prowler API.
+
+
+API key authentication in Prowler Cloud provides an alternative to JWT tokens and empowers automation, CI/CD pipelines, and third-party integrations. This guide explains how to create, manage, and safeguard API keys when working with the Prowler API.
## API Key Advantages
-- **Programmatic access:** Enables automated workflows and scripts to interact with Prowler App.
+- **Programmatic access:** Enables automated workflows and scripts to interact with Prowler Cloud.
- **Long-lived authentication:** Allows optional expiration dates, with a default of 1 year.
- **Granular control:** Supports multiple keys with distinct names and purposes.
- **Secure automation:** Simplifies safe integration into CI/CD pipelines and infrastructure-as-code tooling.
@@ -20,7 +23,7 @@ API key authentication in Prowler App provides an alternative to JWT tokens and
API keys provide a secure authentication mechanism for accessing the Prowler API:
-1. API keys are created through Prowler App with a user-defined name and optional expiration date.
+1. API keys are created through Prowler Cloud with a user-defined name and optional expiration date.
2. The full API key appears only once upon creation and cannot be retrieved later.
3. Each API key consists of a prefix (visible in the interface) and an encrypted secret portion.
4. Requests include the API key in the header as `Authorization: Api-Key `.
@@ -64,13 +67,13 @@ Creating, viewing, or managing API keys requires the **MANAGE_ACCOUNT** RBAC per
Without this permission, the API Keys section remains hidden. Access requests should be routed through the tenant administrator.
-For more information about RBAC permissions, refer to the [Prowler App RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
+For more information about RBAC permissions, refer to the [Prowler Cloud RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
## Creating API Keys
-Follow these steps to create an API key in Prowler App:
+Follow these steps to create an API key in Prowler Cloud:
-1. Navigate to **Profile** → **Account** in Prowler App.
+1. Navigate to **Profile** → **Account** in Prowler Cloud.
2. Select the **Create API Key** button.

@@ -207,7 +210,7 @@ When using API keys in CI/CD pipelines:
* Ensure the key has not been revoked by checking the Revoked column in the API Keys list.
* Confirm that the key has not expired by reviewing the expiration date.
* Confirm that the correct API key format is in use, including both prefix and secret portions.
-* Verify that the key prefix matches what is displayed in Prowler App.
+* Verify that the key prefix matches what is displayed in Prowler Cloud.
### API Key Not Working After Creation
diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths-active-queries.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths-active-queries.mdx
new file mode 100644
index 0000000000..d5ed09c60b
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-app-attack-paths-active-queries.mdx
@@ -0,0 +1,41 @@
+---
+title: "Active Queries"
+sidebarTitle: "Active Queries"
+description: "Focus on the Attack Paths queries active in the environment: Prowler Cloud and Prowler Private Cloud record query results after each scan and hide confirmed-empty queries from the selector."
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx";
+
+
+
+
+
+Active Queries extends the base [Attack Paths](/user-guide/tutorials/prowler-app-attack-paths) feature with capabilities that rely on managed scan infrastructure. This capability is available only in Prowler Cloud and Prowler Private Cloud.
+
+## Focusing on Queries with Data
+
+Running an Attack Paths query against a scan that contains no matching pattern returns an empty graph. Without automatic filtering, identifying the queries that apply to an account means opening each one and checking whether it produces a result. Running the RDS inventory query on an account with no RDS instances, for example, returns a "No data found" message.
+
+
+
+Prowler Cloud removes that trial and error. At the end of each scan, Prowler Cloud records which built-in queries returned data. The query selector then hides the queries confirmed empty for the selected scan, so only the queries that surface a real path remain visible. Following the example above, the RDS inventory query no longer appears in the selector.
+
+
+
+A query stays available whenever its result is not a confirmed empty graph:
+
+- **Errored queries** remain listed. An error is not the same as an empty result and still requires investigation.
+- **Unknown queries** remain listed. Their result for the scan has not been recorded yet.
+- **Parameterized queries** remain listed. Their output depends on the input values provided at run time.
+
+## Browsing the Full Query Catalog on Prowler Hub
+
+The query selector shows the queries relevant to the selected scan, not the entire catalog. To review every built-in Attack Paths query, including the ones hidden for a given scan, browse the complete catalog on [Prowler Hub](https://hub.prowler.com).
+
+Prowler Hub lists each query with its name, description, and the technique it detects, so security teams can plan coverage and understand detection scope without running a scan first.
+
+## Related Pages
+
+- [Attack Paths](/user-guide/tutorials/prowler-app-attack-paths) - Run built-in and custom queries and explore the resulting graph.
+- [Attack Paths Queries](/developer-guide/attack-paths-queries) - Write and maintain openCypher queries in the Developer Guide.
diff --git a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
index 0002bb6f44..1b1013e41a 100644
--- a/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
+++ b/docs/user-guide/tutorials/prowler-app-attack-paths.mdx
@@ -4,9 +4,12 @@ description: "Use graph-based analysis in Prowler App to detect privilege escala
---
import { VersionBadge } from "/snippets/version-badge.mdx";
+import { AppliesTo } from "/snippets/applies-to.mdx";
+
+
Attack Paths analyzes relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited by threat actors.
By mapping these relationships as a graph, Attack Paths reveals risks that individual security checks cannot detect on their own, such as an IAM role that can escalate its own permissions, or a chain of policies that grants unintended access to sensitive resources.
@@ -20,7 +23,7 @@ By mapping these relationships as a graph, Attack Paths reveals risks that indiv
The following prerequisites are required for Attack Paths:
-- **An AWS provider is configured** with valid credentials in Prowler App. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
+- **An AWS provider is configured** with valid credentials in Prowler Cloud. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
- **At least one scan has completed** on the configured AWS provider and produced graph data. Attack Paths scans run automatically alongside regular security scans, no separate configuration is required.
## How Attack Paths Scans Work
@@ -92,6 +95,12 @@ To choose a query, click the dropdown and select from the available options. Eac
Once selected, a description panel appears below the dropdown with more context about the query.
+
+ In Prowler Cloud and Prowler Private Cloud, the query selector hides queries
+ confirmed empty for the selected scan, so only queries that return data remain
+ visible. See [Active Queries](/user-guide/tutorials/prowler-app-attack-paths-active-queries).
+
+
## Configuring Query Parameters
Some queries accept optional or required parameters to narrow the scope of the analysis. When a query has parameters, a form appears below the query description.
@@ -175,112 +184,16 @@ RETURN r.name AS role_name, r.arn AS role_arn, p.arn AS trusted_service
LIMIT 25
```
-### Working with List-Typed Properties
+### Graph Schema and Advanced Patterns
-Some Cartography node properties carry a list of values, such as `action`, `resource`, `notaction`, and `notresource` on `AWSPolicyStatement` nodes, the algorithms on `KMSKey`, the container-definition lists on `ECSContainerDefinition`, and many others. The Attack Paths graph models each such property as a set of child item nodes connected to the parent by a typed edge. To read the values, traverse the edge; the parent does not carry the list as a single field.
+Custom queries traverse the same Cartography graph the built-in queries use. Node labels, relationships, and properties follow the upstream [Cartography AWS Schema](https://cartography-cncf.github.io/cartography/modules/aws/schema.html), enriched by Prowler with `ProwlerFinding` nodes linked through `HAS_FINDING`, `Internet` exposure nodes, and list-typed properties such as `action` and `resource` modeled as child item nodes.
-The naming convention for any list-typed property on a parent label is:
-
-- **Child label:** `Item`. Example: `AWSPolicyStatement.resource` resolves to `AWSPolicyStatementResourceItem`.
-- **Edge type:** `HAS_`. Example: `resource` resolves to `HAS_RESOURCE`.
-- **Child property:** `value` for scalar lists (one string per list element). List-of-dict properties (rare; for example `SecretsManagerSecretVersion.tags`) carry the original dict keys as named fields on the child node.
-
-To express "at least one item in the list satisfies a predicate", traverse the `HAS_*` edge in its own `MATCH` clause and apply the predicate in the attached `WHERE`. `RETURN DISTINCT` collapses duplicate parent rows produced when multiple child items satisfy the filter:
-
-```cypher
-MATCH (stmt:AWSPolicyStatement {effect: 'Allow'})
-MATCH (stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem)
-WHERE toLower(a.value) STARTS WITH 's3:get'
- OR toLower(a.value) STARTS WITH 's3:list'
-RETURN DISTINCT stmt
-LIMIT 25
-```
-
-To check whether every item in the list satisfies a predicate, count the counter-examples and require zero, together with a guard that ensures at least one item is attached. This is the one case where the pattern-comprehension form is the right tool:
-
-```cypher
-MATCH (stmt:AWSPolicyStatement)
-WHERE size([
- (stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem)
- WHERE NOT toLower(a.value) STARTS WITH 's3:'
- | a
- ]) = 0
- AND size([(stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem) | a]) > 0
-RETURN stmt
-LIMIT 25
-```
-
-For the "is any item of this list a substring of a dynamic value" case, such as "does any resource pattern in this policy match a target role ARN", add the `HAS_*` traversal as its own `MATCH` and check the substring relationship between the item value and the dynamic node in `WHERE`:
-
-```cypher
-MATCH (role:AWSRole)
-WHERE role.name = 'Admin'
-MATCH (principal:AWSPrincipal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement {effect: 'Allow'})
-MATCH (stmt)-[:HAS_RESOURCE]->(r:AWSPolicyStatementResourceItem)
-WHERE r.value = '*'
- OR r.value CONTAINS role.name
- OR role.arn CONTAINS r.value
-RETURN DISTINCT principal.arn AS principal, stmt, role
-LIMIT 25
-```
-
-To return the list of values directly, collect them from the child items:
-
-```cypher
-MATCH (stmt:AWSPolicyStatement {effect: 'Allow'})
-OPTIONAL MATCH (stmt)-[:HAS_ACTION]->(a:AWSPolicyStatementActionItem)
-RETURN stmt, collect(a.value) AS actions
-LIMIT 25
-```
-
-### Working with JSON-Encoded Properties
-
-Some Cartography properties represent nested objects, most notably `condition` on `AWSPolicyStatement` and `S3PolicyStatement` nodes. In the Attack Paths graph, object-typed properties are stored as JSON-encoded strings to keep the schema portable across graph backends. The value looks like:
-
-```
-'{"StringEquals":{"aws:SourceAccount":"123456789012"}}'
-```
-
-There is no JSON parser available at query time, so use `CONTAINS` for substring checks against keys or known values:
-
-```cypher
-MATCH (stmt:AWSPolicyStatement)
-WHERE stmt.effect = 'Allow'
- AND stmt.condition CONTAINS '"aws:SourceAccount"'
-RETURN stmt
-LIMIT 25
-```
-
-When a query needs to inspect the structured members of a condition (for example, evaluate every operator and key), fetch the rows first and parse the JSON in application code. Cypher cannot navigate JSON object keys or values.
-
-### Tips for Writing Queries
-
-- Start small with `LIMIT` to inspect the shape of the data before broadening the pattern.
-- Traverse `HAS_*` edges to reach list-typed property values (for example `action`, `resource`). The parent node does not carry the list as a single field; see [Working with List-Typed Properties](#working-with-list-typed-properties) for the patterns.
-- On large scans, avoid broad disconnected patterns such as `MATCH (a:Label), (b:OtherLabel)`. Bind one side with a selective predicate first, and use `WITH DISTINCT` between expanding traversals when duplicates are possible.
-- Use `RETURN` projections (`RETURN n.name, n.region`) instead of returning whole nodes to keep responses compact.
-- Combine resource nodes with `ProwlerFinding` nodes via `HAS_FINDING` to correlate misconfigurations with the affected resources.
-- When a query times out or returns no rows, simplify the pattern step by step until the first variant runs successfully, then add constraints back.
-
-### Cartography Schema Reference
-
-Attack Paths graphs are populated by [Cartography](https://github.com/cartography-cncf/cartography), an open-source graph ingestion framework. The node labels, relationship types, and properties available in custom queries follow the upstream Cartography schema for the corresponding provider.
-
-For the complete catalogue of node labels and relationships available in custom queries, refer to the official Cartography schema documentation:
-
-- **AWS:** [Cartography AWS Schema](https://cartography-cncf.github.io/cartography/modules/aws/schema.html)
-
-In addition to the upstream schema, Prowler enriches the graph with:
-
-- **`ProwlerFinding`** nodes representing Prowler check results, linked to affected resources via `HAS_FINDING` relationships.
-- **`Internet`** nodes used to model exposure paths from the public internet to internal resources.
-- **List-typed properties** such as `action` or `resource` on `AWSPolicyStatement`, the algorithm lists on `KMSKey`, and similar lists on other node types are modeled as child item nodes linked by typed `HAS_*` edges. See [Working with List-Typed Properties](#working-with-list-typed-properties) for the read pattern.
-- **Object-typed properties** such as `condition` on `AWSPolicyStatement` are stored as JSON-encoded strings. See [Working with JSON-Encoded Properties](#working-with-json-encoded-properties) for the read pattern.
+For the complete reference, including the graph model, list-typed and JSON-encoded properties, performance guidance, and openCypher compatibility rules, see [Attack Paths Queries](/developer-guide/attack-paths-queries) in the Developer Guide.
AI assistants connected through Prowler MCP Server can fetch the exact
Cartography schema for the active scan via the
- `prowler_app_get_attack_paths_cartography_schema` tool. This guarantees that
+ `prowler_get_attack_paths_cartography_schema` tool. This guarantees that
generated queries match the schema version pinned by the running Prowler
release.
@@ -424,10 +337,10 @@ Attack Paths capabilities are also available through the [Prowler MCP Server](/g
The following MCP tools are available for Attack Paths:
-- **`prowler_app_list_attack_paths_scans`** - List and filter Attack Paths scans.
-- **`prowler_app_list_attack_paths_queries`** - Discover available queries for a completed scan.
-- **`prowler_app_run_attack_paths_query`** - Execute a query and retrieve graph results with nodes and relationships.
-- **`prowler_app_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema for custom openCypher queries.
+- **`prowler_list_attack_paths_scans`** - List and filter Attack Paths scans.
+- **`prowler_list_attack_paths_queries`** - Discover available queries for a completed scan.
+- **`prowler_run_attack_paths_query`** - Execute a query and retrieve graph results with nodes and relationships.
+- **`prowler_get_attack_paths_cartography_schema`** - Retrieve the Cartography graph schema for custom openCypher queries.
### Example Questions
diff --git a/docs/user-guide/tutorials/prowler-app-finding-groups.mdx b/docs/user-guide/tutorials/prowler-app-finding-groups.mdx
index 2efb27fe13..b8f0da0e2b 100644
--- a/docs/user-guide/tutorials/prowler-app-finding-groups.mdx
+++ b/docs/user-guide/tutorials/prowler-app-finding-groups.mdx
@@ -4,9 +4,12 @@ description: 'Group security findings by check in Prowler App to cut noise, prio
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
+
+
Finding Groups transforms security findings triage by grouping them by check instead of displaying a flat list. This dramatically reduces noise and enables faster, more effective prioritization.
## Triage Challenges with Flat Finding Lists
@@ -112,7 +115,7 @@ This provides full context without leaving the drawer.
## Getting Started
-1. Navigate to the **Findings** section in Prowler Cloud/App.
+1. Navigate to the **Findings** section in Prowler Cloud.
2. Toggle to the **Grouped View** to see findings organized by check.
3. Select any group row to expand and see affected resources.
4. Select a resource to open the detail drawer with full context.
diff --git a/docs/user-guide/tutorials/prowler-app-findings-triage.mdx b/docs/user-guide/tutorials/prowler-app-findings-triage.mdx
index 6c158f380d..15233e85c6 100644
--- a/docs/user-guide/tutorials/prowler-app-findings-triage.mdx
+++ b/docs/user-guide/tutorials/prowler-app-findings-triage.mdx
@@ -113,7 +113,7 @@ Make sure the row is an individual finding row. Finding Groups rows do not show
### Changes cannot be saved
-Confirm that the user role has **Manage Scans** permission. Self-hosted Prowler App does not support Findings Triage writes.
+Confirm that the user role has **Manage Scans** permission. Prowler Local Server does not support Findings Triage writes.
### Resolved or Reopened is missing from the selector
diff --git a/docs/user-guide/tutorials/prowler-app-jira-integration.mdx b/docs/user-guide/tutorials/prowler-app-jira-integration.mdx
index c9fd3751e4..f539aaff4f 100644
--- a/docs/user-guide/tutorials/prowler-app-jira-integration.mdx
+++ b/docs/user-guide/tutorials/prowler-app-jira-integration.mdx
@@ -3,14 +3,17 @@ title: "Prowler App Jira Integration"
description: "Configure Prowler App's Jira integration to send security findings as work items with full remediation guidance and streamline security incident tracking."
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-Prowler App enables automatic export of security findings to Jira, providing seamless integration with Atlassian's work item tracking and project management platform. This comprehensive guide demonstrates how to configure and manage Jira integrations to streamline security incident management and enhance team collaboration across security workflows.
+
-Integrating Prowler App with Jira provides:
+Prowler Cloud enables automatic export of security Findings to Jira, providing seamless integration with Atlassian's work item tracking and project management platform. This comprehensive guide demonstrates how to configure and manage Jira integrations to streamline security incident management and enhance team collaboration across security workflows.
-* **Streamlined management:** Convert security findings directly into actionable Jira work items
+Integrating Prowler Cloud with Jira provides:
+
+* **Streamlined management:** Convert security Findings directly into actionable Jira work items
* **Enhanced team collaboration:** Leverage existing project management workflows for security remediation
* **Automated ticket creation:** Reduce manual effort in tracking and assigning security work items
@@ -18,14 +21,24 @@ Integrating Prowler App with Jira provides:
When enabled and configured:
-1. Security findings can be manually sent to Jira from the Findings table.
-2. Each finding creates a Jira work item with all the check's metadata, including guidance on how to remediate it.
+1. Select one or more complete Finding Groups, or expand a Finding Group and select multiple Findings, from the Findings table.
+2. Send the selection to Jira in one action.
+3. Choose how Jira issues are created:
+ * **Grouped issue:** Create one Jira issue that contains all selected Findings from the Finding Group.
+ * **Separate issues:** Create one Jira issue for each selected Finding. Each Finding represents one affected resource.
+4. Review the Finding Group summary and affected-resource details in Jira, then use the link at the bottom of the issue to open the complete Finding Group in Prowler Cloud.
+
+## Prerequisites
+
+
+
+Configuring and using the Jira integration requires the **Manage Integrations** permission. The Jira integration is tenant-wide, so it does not require **Unlimited Visibility** or any specific Provider Group. Findings sent to Jira are still limited to the providers the role can access.
## Configuration
-To configure Jira integration in Prowler App:
+To configure Jira integration in Prowler Cloud:
-1. Navigate to **Integrations** in the Prowler App interface
+1. Navigate to **Integrations** in Prowler Cloud
2. Locate the **Jira** card and click **Manage**, then select **Add integration**

@@ -43,16 +56,68 @@ To generate a Jira API token, visit: https://id.atlassian.com/manage-profile/sec
-Once configured successfully, the integration is ready to send findings to Jira.
+Once configured successfully, the integration is ready to send Findings to Jira.
## Sending Findings to Jira
-### Manual Export
+Prowler Cloud can send a complete Finding Group or a selection of Findings within a group to Jira in one action.
-To manually send individual findings to Jira:
+### Sending a Complete Finding Group
-1. Navigate to the **Findings** section in Prowler App
-2. Select one finding you want to export
+
+
+To send every Finding in a Finding Group:
+
+1. Navigate to **Findings** in Prowler Cloud.
+2. Select one or more Finding Groups.
+3. Open the bulk actions menu and click **Send Finding Group to Jira**.
+
+ 
+
+4. Select the Jira project and issue type.
+5. Choose an issue creation mode:
+ * **Create one Jira issue for all selected Findings in this Finding Group:** Keeps the complete Finding Group in one Jira issue.
+ * **Create separate Jira issues:** Creates one Jira issue per selected Finding so that each affected resource can be tracked independently.
+6. Click **Send to Jira**.
+
+ 
+
+### Sending Multiple Findings from a Group
+
+
+
+To send only specific affected resources:
+
+1. Expand a Finding Group.
+2. Select the Findings to send. Each Finding represents one affected resource.
+3. Open the bulk actions menu and click **Send Findings to Jira**.
+4. Select the Jira project, issue type, and grouped or separate issue creation mode.
+5. Click **Send to Jira**.
+
+ 
+
+### Reviewing the Jira Issue
+
+A grouped Jira issue starts with Finding Group summary information. This section identifies the check and provides context such as the check title and ID, severity, status, provider, service, number of affected failing resources, last-seen time, failure duration, and risk.
+
+
+
+The affected-resources table lists the selected Findings included in the Jira issue. Each row represents an affected resource and includes the information needed to identify and triage it, such as resource and provider identifiers, provider, service, status, severity, region, last-seen time, failure duration, and triage status.
+
+
+
+At the bottom of the affected-resources table, click **View this Finding Group in Prowler Cloud** to open the complete Finding Group in Prowler Cloud. The link opens the group, not only the Findings included in the Jira issue.
+
+
+
+### Sending One Finding
+
+
+
+To manually send individual Findings to Jira:
+
+1. Navigate to the **Findings** section in Prowler Cloud
+2. Select one Finding you want to export
3. Click the action button on the table row and select **Send to Jira**
4. Select the Jira integration and project
5. Click **Send to Jira**
@@ -66,8 +131,8 @@ Monitor and manage your Jira integrations through the management interface:
1. Review configured integrations in the integrations dashboard
2. Each integration displays:
- - **Connection Status:** Connected or Disconnected indicator
- - **Instance Information:** Jira domain and last checked timestamp
+ * **Connection Status:** Connected or Disconnected indicator
+ * **Instance Information:** Jira domain and last checked timestamp
### Actions
@@ -86,7 +151,7 @@ Each Jira integration provides management actions through dedicated buttons:
Certain Jira issue types (such as Epic) may require mandatory custom fields that Prowler does not currently populate when creating work items. If a selected issue type enforces required fields beyond the standard set (e.g., "Team", "Epic Name"), the work item creation will fail.
-To avoid this, select an issue type that does not require additional custom fields — **Task**, **Bug**, or **Story** typically work without restrictions. If unsure which issue types are available for a project, Prowler automatically fetches and displays them in the "Issue Type" selector when sending a finding.
+To avoid this, select an issue type that does not require additional custom fields — **Task**, **Bug**, or **Story** typically work without restrictions. If unsure which issue types are available for a project, Prowler automatically fetches and displays them in the "Issue Type" selector when sending a Finding.
Support for custom field mapping is planned for a future release.
@@ -96,9 +161,9 @@ Support for custom field mapping is planned for a future release.
### Connection test fails
-- Verify Jira instance domain is correct and accessible
-- Confirm API token or credentials are valid
-- Ensure API access is enabled in Jira settings and the needed scopes are granted
+* Verify Jira instance domain is correct and accessible
+* Confirm API token or credentials are valid
+* Ensure API access is enabled in Jira settings and the needed scopes are granted
### Check task status (API)
@@ -111,7 +176,7 @@ Replace `http://localhost:8080` with the base URL where your Prowler API is acce
1) Get an access token (replace email and password):
-```
+```bash
curl --location 'http://localhost:8080/api/v1/tokens' \
--header 'Content-Type: application/vnd.api+json' \
--header 'Accept: application/vnd.api+json' \
@@ -128,7 +193,7 @@ curl --location 'http://localhost:8080/api/v1/tokens' \
2) List tasks filtered by the Jira task (`integration-jira`) using the access token:
-```
+```bash
curl --location --globoff 'http://localhost:8080/api/v1/tasks?filter[name]=integration-jira' \
--header 'Accept: application/vnd.api+json' \
--header 'Authorization: Bearer ACCESS_TOKEN' | jq
@@ -141,7 +206,7 @@ If you don't have `jq` installed, run the command without `| jq`.
3) Share the output so we can help. A typical result will look like:
-```
+```json
{
"links": {
"first": "https://api.dev.prowler.com/api/v1/tasks?page%5Bnumber%5D=1",
@@ -210,5 +275,5 @@ If you don't have `jq` installed, run the command without `| jq`.
How to read it:
-- "created_count": number of Jira issues successfully created.
-- "failed_count": number of Jira issues that could not be created. If `failed_count > 0` or the issue does not appear in Jira, please contact us so we can assist while detailed logs are not available through the UI.
+* "created_count": number of Jira issues successfully created.
+* "failed_count": number of Jira issues that could not be created. If `failed_count > 0` or the issue does not appear in Jira, please contact us so we can assist while detailed logs are not available through the UI.
diff --git a/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx b/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
index c71dc4b6d4..d81a147337 100644
--- a/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
+++ b/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
@@ -129,6 +129,25 @@ To connect a provider:
3. Configure in Lighthouse AI:
- **API Key**: OpenRouter API key
- **Base URL**: `https://openrouter.ai/api/v1`
+
+ ### Base URL Validation
+
+ To prevent server-side request forgery (SSRF), Prowler API validates the base URL before connecting to it:
+
+ - The URL must use HTTPS.
+ - The host must resolve to a public IP address. Private, loopback, link-local, and cloud metadata addresses are rejected.
+
+
+ This validation can break configurations that point to internal endpoints, such as a self-hosted Ollama server. This is intentional: it fixes a security issue where the Prowler API could be directed to internal services. Internal endpoints must now be allowed explicitly through `LIGHTHOUSE_AI_OPENAI_COMPATIBLE_ALLOWED_HOSTS`.
+
+
+ To allow internal endpoints, set a comma-separated list of hostnames or IP addresses in the Prowler API environment (for Docker Compose deployments, the shared `.env` file):
+
+ ```bash
+ LIGHTHOUSE_AI_OPENAI_COMPATIBLE_ALLOWED_HOSTS=custom-openai.internal,10.0.0.20
+ ```
+
+ Hosts in this list skip the public-endpoint validation. HTTPS is still required, so the endpoint needs a certificate the Prowler API trusts.
diff --git a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
index 50859f47f1..d7f9b2636f 100644
--- a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
+++ b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
@@ -20,7 +20,7 @@ Behind the scenes, Lighthouse AI works as follows:
- The agent accesses Prowler data through [Prowler MCP](https://docs.prowler.com/getting-started/products/prowler-mcp), which exposes tools from multiple sources, including:
- Prowler Hub
- Prowler Docs
- - Prowler App
+ - Prowler Local Server
- Instead of calling every tool directly, the agent uses two meta-tools:
- `describe_tool` to retrieve a tool schema and parameter requirements.
- `execute_tool` to run the selected tool with the required input.
diff --git a/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx
index e3404c3b65..a2ce0d9deb 100644
--- a/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx
+++ b/docs/user-guide/tutorials/prowler-app-multi-tenant.mdx
@@ -4,10 +4,13 @@ description: 'Use Prowler App Organizations to isolate providers, scans, finding
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-Prowler App supports multi-tenancy through **Organizations**, allowing users to belong to multiple isolated environments within a single account. Each organization maintains its own providers, scans, findings, and user memberships, ensuring complete data separation between teams or business units.
+
+
+Prowler Cloud supports multi-tenancy through **Organizations**, allowing users to belong to multiple isolated environments within a single account. Each organization maintains its own providers, scans, findings, and user memberships, ensuring complete data separation between teams or business units.
## Key Concepts
@@ -129,7 +132,7 @@ When invited to join an organization, the invited user receives a link to accept
1. Open the invitation link.
-2. If already authenticated, the invitation is accepted automatically and the user is redirected to Prowler App.
+2. If already authenticated, the invitation is accepted automatically and the user is redirected to Prowler Cloud.
3. If not authenticated, choose **I have an account -- Sign in**, authenticate with existing credentials, and the invitation is accepted upon sign-in.
diff --git a/docs/user-guide/tutorials/prowler-app-mute-findings.mdx b/docs/user-guide/tutorials/prowler-app-mute-findings.mdx
index 0581d05cb7..b51e78d9e9 100644
--- a/docs/user-guide/tutorials/prowler-app-mute-findings.mdx
+++ b/docs/user-guide/tutorials/prowler-app-mute-findings.mdx
@@ -3,10 +3,13 @@ title: 'Advanced Mutelist (YAML) in Prowler App'
description: 'Write YAML-based mutelist rules in Prowler App to mute findings across checks, regions, resources, and tags using regex patterns for complex environments.'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-Prowler App allows users to mute specific findings to focus on the most critical security issues. This guide demonstrates how to use the Advanced Mutelist feature with YAML configuration for complex, pattern-based muting rules.
+
+
+Prowler Cloud allows users to mute specific findings to focus on the most critical security issues. This guide demonstrates how to use the Advanced Mutelist feature with YAML configuration for complex, pattern-based muting rules.
For muting individual findings without YAML configuration, use [Simple Mutelist](/user-guide/tutorials/prowler-app-simple-mutelist) to mute findings directly from the Findings table.
@@ -32,8 +35,8 @@ Advanced Mutelist requires the **Manage Account** permission. See [RBAC Administ
Before muting findings, ensure:
-- Valid access to Prowler App with appropriate permissions
-- A provider added to the Prowler App
+- Valid access to Prowler Cloud with appropriate permissions
+- A provider added to Prowler Cloud
- Understanding of the security implications of muting specific findings
@@ -44,7 +47,7 @@ Muting findings does not resolve underlying security issues. Review each finding
To configure Advanced Mutelist:
-1. Log into Prowler App
+1. Log into Prowler Cloud
2. Navigate to the Providers page

3. Connect a provider to enable Mutelist configuration
@@ -164,7 +167,7 @@ Mutelist:
- "*"
Resources:
- "app-vnet-peering-*"
- Description: "Mute App Function Vnet findings related with the reources pattern"
+ Description: "Mute App Function Vnet findings related with the resources pattern"
```
#### Azure Resource Group Muting
@@ -424,7 +427,7 @@ Mutelist:
### Priority: Advanced vs. Simple Mutelist
-When both Advanced Mutelist (YAML) and [Simple Mutelist](/user-guide/tutorials/prowler-app-simple-mutelist) rules match the same finding, the **Advanced Mutelist takes higher priority**. The finding will be muted with the reason "Muted by mutelist". If a finding is not matched by the Advanced Mutelist but matches a Simple Mutelist rule, the Simple rule's custom justification is used instead.
+When both Advanced Mutelist and [Simple Mutelist](/user-guide/tutorials/prowler-app-simple-mutelist) rules match the same finding, the **Advanced Mutelist takes higher priority**. The finding will be muted with the reason "Muted by mutelist". If a finding is not matched by the Advanced Mutelist but matches a Simple Mutelist rule, the Simple rule's custom justification is used instead.
### Best Practices
@@ -437,7 +440,7 @@ When both Advanced Mutelist (YAML) and [Simple Mutelist](/user-guide/tutorials/p
### Validation
-Prowler App validates your mutelist configuration and will display errors for:
+Prowler Cloud validates the mutelist configuration and will display errors for:
- Invalid YAML syntax
- Missing required fields
diff --git a/docs/user-guide/tutorials/prowler-app-rbac.mdx b/docs/user-guide/tutorials/prowler-app-rbac.mdx
index c9266b67bc..2550bdf9dd 100644
--- a/docs/user-guide/tutorials/prowler-app-rbac.mdx
+++ b/docs/user-guide/tutorials/prowler-app-rbac.mdx
@@ -4,10 +4,13 @@ description: 'Invite users, assign roles, and configure Role-Based Access Contro
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-**Prowler App** supports multiple users within a single tenant, enabling seamless collaboration by allowing team members to easily share insights and manage security findings.
+
+
+**Prowler Cloud** supports multiple users within a single tenant, enabling seamless collaboration by allowing team members to easily share insights and manage security findings.
[Roles](#roles) help you control user permissions, determining what actions each user can perform and the data they can access within Prowler. By default, each account includes an immutable **admin** role, ensuring that your account always retains administrative access.
@@ -66,7 +69,7 @@ To remove **another** user from your organization, use the [_Expel from organiza
#### Inviting Users
-Please be aware that at this time, an email address can only be associated with a single Prowler account_.
+Please be aware that at this time, an email address can only be associated with a single Prowler account.
Follow these steps to invite a user to your account:
@@ -125,7 +128,7 @@ To resend the invitation to the user, it is necessary to explicitly **delete the
## Managing Groups and Roles
-The Roles section in Prowler is designed to facilitate the assignment of custom user privileges. This section allows administrators to define roles with specific permissions for Prowler administrative tasks and Account visibility.
+Roles combine administrative permissions with provider visibility. Administrative permissions control the actions a role can perform. Provider Groups and Unlimited Visibility control the providers, resources, findings, scans, compliance results, and integrations the role can access.
**Only users that have the _Manage Account_ or _admin_ permission can access this section.**
@@ -133,47 +136,59 @@ The Roles section in Prowler is designed to facilitate the assignment of custom
### Provider Groups
-Provider Groups control visibility across specific providers. When creating a new role, you can assign specific groups to define their Provider visibility. This ensures that users with that role have access only to the Providers that are required.
+Provider Groups limit visibility to selected providers. Assigning one or more Provider Groups to a role grants access to the providers in those groups and their resources, findings, scans, and compliance results.
-By default, a new user role does not have visibility into any group.
+New roles have no provider visibility by default. Assign at least one Provider Group or enable **Unlimited Visibility** before assigning the role to users who need access to provider data.
-Alternatively, to grant the role unlimited visibility across all providers, check the Grant Unlimited Visibility checkbox.
+**Unlimited Visibility** grants organization-wide visibility across every provider, regardless of the Provider Groups assigned to the role. It does not grant administrative permissions.
+
+#### Integration Visibility
+
+
+
+Integrations follow the visibility of the providers attached to them: a role can see an integration when it can access at least one of its providers, and only the providers visible to that role are listed on the integration. Editing or deleting an integration attached to providers outside the visibility of the role is not allowed. Integrations that are not attached to any provider, such as Jira, are tenant-wide and remain available to every role with the **Manage Integrations** permission.
#### Creating a Provider Group
Follow these steps to create a provider group in your account:
-1. Navigate to **Provider Groups** from the side menu..
+1. Click **Providers** in the side menu.
-2. In this view you can select the provider groups you want to assign to one or more roles.
+2. Select the **Provider Groups** tab.
-3. Click the **Create Group** button on the center of the screen.
+3. Enter a group name in the **Create a new provider group** form.
-
+4. Select the providers that the group controls. Optionally, select the roles that should use the group.
+
+5. Click **Create Group**.
+
+
#### Editing a Provider Group
Follow these steps to edit a provider group on your account:
-1. Navigate to **Provider Groups** from the side menu.
+1. Click **Providers** in the side menu and select the **Provider Groups** tab.
-2. Click the edit button of the provider group you want to modify.
+2. Open the actions menu for the Provider Group and click **Edit Provider Group**.
-
+
-3. Change the provider group parameters you need and save the changes.
+3. Update the group name, providers, or roles, and save the changes.
-
+
#### Removing a Provider Group
-Follow these steps to remove a provider group of your account:
+Follow these steps to remove a provider group from your account:
-1. Navigate to **Provider Groups** from the side menu.
+1. Click **Providers** in the side menu and select the **Provider Groups** tab.
-2. Click on the delete button of the provider group you want to remove.
+2. Open the actions menu for the Provider Group and click **Delete Provider Group**.
-
+3. Confirm the deletion.
+
+
### Roles
@@ -183,19 +198,20 @@ Follow these steps to create a role for your account:
1. Navigate to **Roles** from the side menu.
-2. Click on the **Add Role** button on the top right-hand corner of the screen.
+2. Click **Add Role**.
-
+3. Enter the role name and select the required administrative permissions.
-3. In the Add Role screen, enter the role name, the administration permissions and the groups of providers to which the Role will have access to.
-
-4. In the Groups and Account Visibility section, you will see a list of available groups with checkboxes next to them. To assign a group to the user role, simply click the checkbox next to the group name. If you need to assign multiple groups, repeat the process for each group you wish to add.
+4. Configure **Visibility**:
+ - To grant organization-wide visibility, select **Enable Unlimited Visibility for this role**.
+ - To limit visibility, leave Unlimited Visibility cleared and select one or more Provider Groups.
+5. Click **Add Role**.
-To assign read-only access, select only the `Unlimited Visibility` permission when creating the role. Then, go to the Users page and assign this role to the appropriate user.
+To grant read-only access across the organization, enable **Unlimited Visibility** without selecting administrative permissions. Then, assign the role from the **Users** page.
#### Editing a Role
@@ -204,25 +220,21 @@ Follow these steps to edit a role on your account:
1. Navigate to **Roles** from the side menu.
-2. Click on the edit button of the role you want to modify.
+2. Open the actions menu for the role and click **Edit Role**.
-
-
-3. Adjust the settings as needed and save the changes.
-
-
+3. Update the role name, administrative permissions, Unlimited Visibility setting, or Provider Groups.
+4. Save the changes.
#### Removing a Role
-Follow these steps to remove a role of your account:
+Follow these steps to remove a role from your account:
1. Navigate to **Roles** from the side menu.
-2. Click on the delete button of the role you want to remove.
-
-
+2. Open the actions menu for the role and click **Delete Role**.
+3. Confirm the deletion.
## RBAC Administrative Permissions
diff --git a/docs/user-guide/tutorials/prowler-app-s3-integration.mdx b/docs/user-guide/tutorials/prowler-app-s3-integration.mdx
index b744f867c9..fe0ca04f88 100644
--- a/docs/user-guide/tutorials/prowler-app-s3-integration.mdx
+++ b/docs/user-guide/tutorials/prowler-app-s3-integration.mdx
@@ -4,10 +4,13 @@ description: 'Automatically export Prowler App scan results in CSV, HTML, and OC
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-**Prowler App** allows automatic export of scan results to Amazon S3 buckets, providing seamless integration with existing data workflows and storage infrastructure. This comprehensive guide demonstrates configuration and management of Amazon S3 integrations to streamline security finding management and reporting.
+
+
+**Prowler Cloud** allows automatic export of scan results to Amazon S3 buckets, providing seamless integration with existing data workflows and storage infrastructure. This comprehensive guide demonstrates configuration and management of Amazon S3 integrations to streamline security finding management and reporting.
When enabled and configured, scan results are automatically stored in the configured bucket. Results are provided in `csv`, `html` and `json-ocsf` formats, offering flexibility for custom integrations:
@@ -202,7 +205,7 @@ Replace `` with the AWS account ID that contains the IAM role
### Available Templates
-**Prowler App** provides Infrastructure as Code (IaC) templates to automate IAM role setup with S3 integration permissions.
+**Prowler Cloud** provides Infrastructure as Code (IaC) templates to automate IAM role setup with S3 integration permissions.
Templates are optional. Custom IAM roles or static credentials can be used instead.
@@ -260,8 +263,8 @@ If using Prowler's CloudFormation template, execute the following command to upd
- `EnableS3Integration`: Select "true"
- `S3IntegrationBucketName`: Your bucket name
- `S3IntegrationBucketAccountId`: Bucket owner's AWS account ID
-5. In the "Configure stack options" screen, again, leave everything as it is and click on "Next"
-6. Finally, under "Review Prowler", at the bottom click on "Submit"
+5. In the "Configure stack options" screen, again, leave everything as it is and click "Next"
+6. Finally, under "Review Prowler", at the bottom click "Submit"
#### Terraform
@@ -279,7 +282,7 @@ If using Prowler's CloudFormation template, execute the following command to upd
3. Edit `terraform.tfvars` with your specific values:
```hcl
- # Required: External ID from Prowler App
+ # Required: External ID from Prowler Cloud
external_id = "your-unique-external-id-here"
# S3 Integration Configuration
@@ -311,11 +314,11 @@ For detailed information, refer to the [Terraform README](https://github.com/pro
## Configuration
-Once the required permissions are set up, proceed to configure the S3 integration in **Prowler App**.
+Once the required permissions are set up, proceed to configure the S3 integration in **Prowler Cloud**.
1. Navigate to "Integrations"

-2. Locate the Amazon S3 Integration card and click on the "Configure" button
+2. Locate the Amazon S3 Integration card and click the "Configure" button

3. Click the "Add Integration" button

diff --git a/docs/user-guide/tutorials/prowler-app-scan-configuration.mdx b/docs/user-guide/tutorials/prowler-app-scan-configuration.mdx
index ef4cc704e1..2ad5ae1830 100644
--- a/docs/user-guide/tutorials/prowler-app-scan-configuration.mdx
+++ b/docs/user-guide/tutorials/prowler-app-scan-configuration.mdx
@@ -8,7 +8,7 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
-Scan Configuration lets you override, per provider, specific values in the default configuration Prowler's checks use during a scan. Each configuration modifies how specific checks behave, e.g.: thresholds, allowed values, retention windows, and you attach it to the providers that you want to use it on their next scan.
+Scan Configuration lets you override, per provider, specific values in the default configuration Prowler's checks use during a scan. Each configuration can modify how specific checks behave, such as thresholds, allowed values, and retention windows, or exclude checks and services from the scan scope. Attach it to the providers that should use it on their next scan.
@@ -54,6 +54,24 @@ gcp:
storage_min_retention_days: 30
```
+### Limiting the Scan Scope
+
+
+
+Use `excluded_checks` to skip individual checks and `excluded_services` to skip every check in a service for the matching provider type:
+
+```yaml
+aws:
+ excluded_checks:
+ - s3_bucket_public_access
+ excluded_services:
+ - ec2
+```
+
+
+When a Scan Configuration excludes checks or services, Prowler calculates overviews, aggregations, and other result-based information from the reduced scan scope. The displayed information reflects only the checks and services that ran, not a complete assessment of the provider. Consider the applied Scan Configuration when interpreting totals and security posture.
+
+
## Creating a Scan Configuration
diff --git a/docs/user-guide/tutorials/prowler-app-security-hub-integration.mdx b/docs/user-guide/tutorials/prowler-app-security-hub-integration.mdx
index 7551208cc6..6920f1d851 100644
--- a/docs/user-guide/tutorials/prowler-app-security-hub-integration.mdx
+++ b/docs/user-guide/tutorials/prowler-app-security-hub-integration.mdx
@@ -3,12 +3,15 @@ title: "Prowler App AWS Security Hub Integration"
description: "Send Prowler App findings to AWS Security Hub to centralize multi-account visibility, automate archiving, filter by severity, and control ingestion costs."
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-Prowler App enables automatic export of security findings to AWS Security Hub, providing seamless integration with AWS's native security and compliance service. This comprehensive guide demonstrates how to configure and manage AWS Security Hub integrations to centralize security findings and enhance compliance tracking across AWS environments.
+
-Integrating Prowler App with AWS Security Hub provides:
+Prowler Cloud enables automatic export of security findings to AWS Security Hub, providing seamless integration with AWS's native security and compliance service. This comprehensive guide demonstrates how to configure and manage AWS Security Hub integrations to centralize security findings and enhance compliance tracking across AWS environments.
+
+Integrating Prowler Cloud with AWS Security Hub provides:
* **Centralized security visibility:** Consolidate findings from multiple AWS accounts and regions
* **Native AWS integration:** Leverage existing AWS security workflows and compliance frameworks
@@ -31,7 +34,7 @@ Refer to [AWS Security Hub pricing](https://aws.amazon.com/security-hub/pricing/
## Prerequisites
-Before configuring AWS Security Hub Integration in Prowler App, complete these steps:
+Before configuring AWS Security Hub Integration in Prowler Cloud, complete these steps:
### AWS Security Hub Setup
@@ -43,9 +46,9 @@ Configure AWS credentials by following the [AWS authentication setup guide](/use
## Configuration
-To configure AWS Security Hub integration in Prowler App:
+To configure AWS Security Hub integration in Prowler Cloud:
-1. Navigate to **Integrations** in the Prowler App interface
+1. Navigate to **Integrations** in Prowler Cloud
2. Locate the **AWS Security Hub** card and click **Manage**, then select **Add integration**

diff --git a/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx b/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx
index 6006f1aa18..0739f36fcc 100644
--- a/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx
+++ b/docs/user-guide/tutorials/prowler-app-simple-mutelist.mdx
@@ -4,10 +4,13 @@ description: "Mute Prowler App findings individually or in bulk from the Finding
---
import { VersionBadge } from "/snippets/version-badge.mdx";
+import { AppliesTo } from "/snippets/applies-to.mdx";
-Prowler App provides Simple Mutelist, an intuitive way to mute findings directly from the Findings page without writing YAML configuration. This feature streamlines the muting workflow by allowing individual or bulk muting with just a few clicks.
+
+
+Prowler Cloud provides Simple Mutelist, an intuitive way to mute findings directly from the Findings page without writing YAML configuration. This feature streamlines the muting workflow by allowing individual or bulk muting with just a few clicks.
## What Is Simple Mutelist?
diff --git a/docs/user-guide/tutorials/prowler-app-social-login.mdx b/docs/user-guide/tutorials/prowler-app-social-login.mdx
index 007b56b6c3..03769c2895 100644
--- a/docs/user-guide/tutorials/prowler-app-social-login.mdx
+++ b/docs/user-guide/tutorials/prowler-app-social-login.mdx
@@ -4,10 +4,13 @@ description: 'Enable Google and GitHub OAuth authentication in Prowler App by co
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
-**Prowler App** supports social login using Google and GitHub OAuth providers. This document guides you through configuring the required environment variables to enable social authentication.
+
+
+Prowler supports social login using Google and GitHub OAuth providers. In **Prowler Cloud** social login is available out of the box. In **Prowler Local Server**, enable it by configuring the environment variables described in this guide.
## Configuring Social Login Credentials
diff --git a/docs/user-guide/tutorials/prowler-app-sso-entra.mdx b/docs/user-guide/tutorials/prowler-app-sso-entra.mdx
index c7dd4f1c04..3dfb3b0bd0 100644
--- a/docs/user-guide/tutorials/prowler-app-sso-entra.mdx
+++ b/docs/user-guide/tutorials/prowler-app-sso-entra.mdx
@@ -3,7 +3,11 @@ title: 'SAML SSO with Microsoft Entra ID'
description: 'Create and configure a Microsoft Entra ID (Azure AD) enterprise application to enable SAML Single Sign-On for Prowler App users across your organization.'
---
-This page provides instructions for creating and configuring a Microsoft Entra ID (formerly Azure AD) application to use SAML SSO with Prowler App.
+import { AppliesTo } from "/snippets/applies-to.mdx"
+
+
+
+This page provides instructions for creating and configuring a Microsoft Entra ID (formerly Azure AD) application to use SAML SSO with Prowler Cloud.
You can find a walkthrough video [here](https://www.youtube.com/watch?v=zegqm55oJVk).
@@ -29,7 +33,7 @@ You can find a walkthrough video [here](https://www.youtube.com/watch?v=zegqm55o

-6. Enter the "Identifier (Entity ID)" and "Reply URL (Assertion Consumer Service URL)". These values can be obtained from the SAML SSO integration setup in Prowler App. For detailed instructions, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso) page.
+6. Enter the "Identifier (Entity ID)" and "Reply URL (Assertion Consumer Service URL)". These values can be obtained from the SAML SSO integration setup in Prowler Cloud. For detailed instructions, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso) page.

@@ -45,4 +49,4 @@ You can find a walkthrough video [here](https://www.youtube.com/watch?v=zegqm55o

-10. Save the downloaded Metadata XML to a file. To complete the setup, upload this file during the Prowler App integration. (See the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso) page for details).
+10. Save the downloaded Metadata XML to a file. To complete the setup, upload this file during the SAML SSO integration setup in Prowler Cloud. (See the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso) page for details).
diff --git a/docs/user-guide/tutorials/prowler-app-sso-google-workspace.mdx b/docs/user-guide/tutorials/prowler-app-sso-google-workspace.mdx
index a989111e69..bec12c9cd0 100644
--- a/docs/user-guide/tutorials/prowler-app-sso-google-workspace.mdx
+++ b/docs/user-guide/tutorials/prowler-app-sso-google-workspace.mdx
@@ -3,20 +3,25 @@ title: 'SAML SSO with Google Workspace'
description: 'Set up a custom SAML app in Google Admin Console and pair it with Prowler App to give Google Workspace users Single Sign-On access to your organization.'
---
-This page explains how to configure SAML-based Single Sign-On (SSO) in Prowler App using **Google Workspace** as the Identity Provider (IdP). The setup is divided into two parts: create a custom SAML app in Google Admin Console, then complete the configuration in Prowler App.
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
+
+
+
+This page explains how to configure SAML-based Single Sign-On (SSO) in Prowler Cloud using **Google Workspace** as the Identity Provider (IdP). The setup is divided into two parts: create a custom SAML app in Google Admin Console, then complete the configuration in Prowler Cloud.
**Parallel Setup Required**
-Google Admin Console requires the ACS URL and Entity ID from Prowler App, while Prowler App displays these values only after opening the SAML configuration dialog. To work around this, open Prowler App in a separate browser tab, navigate to the profile page, open the "Configure SAML SSO" dialog, and copy the ACS URL and Entity ID before proceeding with the Google configuration.
+Google Admin Console requires the ACS URL and Entity ID from Prowler Cloud, while Prowler Cloud displays these values only after opening the SAML configuration dialog. To work around this, open Prowler Cloud in a separate browser tab, navigate to the profile page, open the "Configure SAML SSO" dialog, and copy the ACS URL and Entity ID before proceeding with the Google configuration.
## Prerequisites
- **Google Workspace**: Super Admin access (or delegated admin with app management permissions).
-- **Prowler App**: Administrator access to the organization (role with "Manage Account" permission).
-- Prowler App version **5.9.0** or later.
+- **Prowler Cloud**: Administrator access to the organization (role with "Manage Account" permission).
+- Prowler version **5.9.0** or later.
---
@@ -45,7 +50,7 @@ On the **Google Identity Provider details** screen:
1. Google displays two options:
- **Option 1**: Click "Download Metadata" to save the XML file directly. This is the recommended approach.
- **Option 2**: Manually copy the **SSO URL**, **Entity ID**, and **Certificate**.
-2. Download the metadata. This file is required to complete the Prowler App configuration in Part B.
+2. Download the metadata. This file is required to complete the configuration in Prowler Cloud (Part B).
3. Click "Continue".

@@ -53,18 +58,18 @@ On the **Google Identity Provider details** screen:
**Save the Metadata File**
-Download and save the IdP metadata XML file before proceeding. This file cannot be easily retrieved later and is required to complete the SAML configuration in Prowler App.
+Download and save the IdP metadata XML file before proceeding. This file cannot be easily retrieved later and is required to complete the SAML configuration in Prowler Cloud.
### Step 4: Configure the Service Provider Details
-Enter the following values obtained from the SAML SSO configuration dialog in Prowler App (see [Part B, Step 1](#step-1-open-the-saml-configuration-dialog) for details on where to find them):
+Enter the following values obtained from the SAML SSO configuration dialog in Prowler Cloud (see [Part B, Step 1](#step-1-open-the-saml-configuration-dialog) for details on where to find them):
| Google Workspace Field | Value |
|------------------------|-------|
-| **ACS URL** | The Assertion Consumer Service (ACS) URL displayed in Prowler App (e.g., `https://api.prowler.com/api/v1/accounts/saml/your-domain.com/acs/`). Self-hosted deployments use a different base URL. |
-| **Entity ID** | The Audience URI displayed in Prowler App (e.g., `urn:prowler.com:sp`). |
+| **ACS URL** | The Assertion Consumer Service (ACS) URL displayed in Prowler Cloud (e.g., `https://api.prowler.com/api/v1/accounts/saml/your-domain.com/acs/`). Prowler Local Server deployments use a different base URL. |
+| **Entity ID** | The Audience URI displayed in Prowler Cloud (e.g., `urn:prowler.com:sp`). |
| **Name ID format** | Select `EMAIL` from the dropdown. |
| **Name ID** | Select `Basic Information > Primary email` from the dropdown. |
@@ -83,7 +88,7 @@ Click "Add mapping" for each entry:
| `Basic Information > First name` | `firstName` | Yes | |
| `Basic Information > Last name` | `lastName` | Yes | |
| `Employee Details > Department` | `userType` | No | Determines the Prowler role. **Case-sensitive.** |
-| `Employee Details > Organization` | `organization` | No | Company name displayed in Prowler App profile. |
+| `Employee Details > Organization` | `organization` | No | Company name displayed in the user profile in Prowler Cloud. |
**Remember the Mapped Fields**
@@ -99,7 +104,7 @@ Click "Finish" to create the SAML app.
**Dynamic Updates**
-Prowler App updates user attributes each time a user logs in. Any changes made in Google Workspace are reflected on the next login.
+Prowler Cloud updates user attributes each time a user logs in. Any changes made in Google Workspace are reflected on the next login.
@@ -109,8 +114,13 @@ Prowler App updates user attributes each time a user logs in. Any changes made i
The `userType` attribute controls which Prowler role is assigned to the user:
- If `userType` matches an existing Prowler role name, the user receives that role automatically.
-- If `userType` does not match any existing role, Prowler App creates a new role with that name **with read-only access** (visibility over all providers, no management permissions). A Prowler administrator can adjust its permissions afterward through the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
-- If `userType` is not set, the user's existing roles are left unchanged.
+- If `userType` does not match any existing role, Prowler Cloud creates a new role with that name **with read-only access** (visibility over all providers, no management permissions). A Prowler administrator can adjust its permissions afterward through the [RBAC Management](/user-guide/tutorials/prowler-app-rbac) tab.
+
+**Fallback Role Without `userType`**
+
+
+
+If `userType` is not set, the user's existing roles are left unchanged. Users without an existing role in that tenant receive a least-privilege `read_only` fallback role until a Prowler administrator assigns another role. If `read_only` already belongs to a role with different permissions, Prowler Cloud checks suffixed names in order, starting with `read_only_0`. It reuses the first role with the fallback permissions or creates the first available name.
The `userType` value is **case-sensitive** - for example, `Backend` and `backend` are treated as different roles.
@@ -149,13 +159,13 @@ If attempting to use the "Test SAML login" option in Google Admin Console and re
---
-## Part B - Prowler App Configuration
+## Part B - Prowler Cloud Configuration
### Step 1: Open the SAML Configuration Dialog
1. Navigate to the profile settings page:
- **Prowler Cloud**: `https://cloud.prowler.com/profile`
- - **Self-hosted**: `http://{your-domain}/profile`
+ - **Prowler Local Server**: `http://{your-domain}/profile`
2. Find the "SAML SSO Integration" card and click "Enable" (or "Update" if already configured).
3. The "Configure SAML SSO" dialog opens, displaying:
- **ACS URL**: The Assertion Consumer Service URL (copy this value for Part A, Step 4). This URL updates dynamically when the email domain is entered.
@@ -163,21 +173,21 @@ If attempting to use the "Test SAML login" option in Google Admin Console and re
- **Name ID Format**: The expected format (`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`).
- **Supported Assertion Attributes**: The list of accepted attributes (`firstName`, `lastName`, `userType`, `organization`).
-
+
### Step 2: Enter the Email Domain and Upload Metadata
-1. Enter the **email domain** for the organization (e.g., `prowler.cloud`). Prowler App uses this domain to identify users who should authenticate via SAML. The ACS URL updates automatically to reflect the configured domain.
+1. Enter the **email domain** for the organization (e.g., `prowler.cloud`). Prowler Cloud uses this domain to identify users who should authenticate via SAML. The ACS URL updates automatically to reflect the configured domain.
2. Upload the **metadata XML file** downloaded in Part A, Step 3.
3. Click "Save".
-
+
### Step 3: Verify the Enabled Status
The "SAML SSO Integration" card should now display a **"Status: Enabled"** indicator with a checkmark, confirming that the configuration is complete.
-
+
---
@@ -215,13 +225,13 @@ To test the `userType` → role mapping, set the **Department** attribute in the
1. Navigate to the Prowler login page.
2. Click "Continue with SAML SSO".
3. Enter an email from the configured domain (e.g., `adrian@prowler.cloud`).
-4. Click "Log in". The browser redirects to Google for authentication and returns to Prowler App upon success.
+4. Click "Log in". The browser redirects to Google for authentication and returns to Prowler Cloud upon success.
-
+
### Verify User Profile and Role Mapping
-After a successful SSO login, the user profile in Prowler App reflects the attributes sent by Google Workspace:
+After a successful SSO login, the user profile in Prowler Cloud reflects the attributes sent by Google Workspace:
- **Name**: Populated from the `firstName` and `lastName` attributes.
- **Role**: Created automatically from the `userType` attribute (e.g., `Backend`). If the role did not exist previously, it is created with read-only access by default.
@@ -231,14 +241,14 @@ After a successful SSO login, the user profile in Prowler App reflects the attri
For more details on role assignment behavior and attribute mapping, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso#configure-attribute-mapping-in-the-idp) page.
-
+
### IdP-Initiated SSO (from Google)
1. Sign in to Google Workspace with an account that has access to the Prowler SAML app.
2. Open the Google Workspace app launcher (the grid icon in the top-right corner of any Google page).
-3. Click the Prowler app tile.
-4. The browser redirects directly to Prowler App, authenticated.
+3. Click the Prowler tile.
+4. The browser redirects directly to Prowler Cloud, authenticated.
For more information on the SSO login flows, refer to the [SAML SSO Configuration](/user-guide/tutorials/prowler-app-sso#idp-initiated-sso) page.
@@ -271,7 +281,7 @@ Prowler does not allow two tenants to share the same email domain. If the domain
**Just-in-Time Provisioning**
-Users who authenticate via SAML for the first time are automatically created in Prowler App. No prior invitation is needed. User attributes (`firstName`, `lastName`, `userType`) are updated on every login from the Google directory.
+Users who authenticate via SAML for the first time are automatically created in Prowler Cloud. No prior invitation is needed. User attributes (`firstName`, `lastName`, `userType`) are updated on every login from the Google directory.
@@ -279,10 +289,10 @@ Users who authenticate via SAML for the first time are automatically created in
## Quick Summary
-1. In **Google Admin Console**, create a custom SAML app using the ACS URL and Entity ID from Prowler App.
+1. In **Google Admin Console**, create a custom SAML app using the ACS URL and Entity ID from Prowler Cloud.
2. Configure **attribute mapping**: `firstName`, `lastName`, and optionally `userType` and `organization`.
3. **Download the metadata XML** from Google.
4. **Enable the app** in Google Workspace for the relevant users or groups.
-5. In **Prowler App**, enter the email domain, upload the metadata XML, and save.
+5. In **Prowler Cloud**, enter the email domain, upload the metadata XML, and save.
6. Verify the SAML SSO Integration shows **"Status: Enabled"**.
7. Test login via "Continue with SAML SSO" on the Prowler login page.
diff --git a/docs/user-guide/tutorials/prowler-app-sso.mdx b/docs/user-guide/tutorials/prowler-app-sso.mdx
index db93a2a951..881ae61ec9 100644
--- a/docs/user-guide/tutorials/prowler-app-sso.mdx
+++ b/docs/user-guide/tutorials/prowler-app-sso.mdx
@@ -4,16 +4,20 @@ description: 'Configure SAML-based Single Sign-On in Prowler App with any Identi
---
import { VersionBadge } from "/snippets/version-badge.mdx"
+import { AppliesTo } from "/snippets/applies-to.mdx"
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
-This guide provides comprehensive instructions to configure SAML-based Single Sign-On (SSO) in Prowler App. This configuration allows users to authenticate using the organization's Identity Provider (IdP).
+
+
+This guide provides comprehensive instructions to configure SAML-based Single Sign-On (SSO) in Prowler Cloud. This configuration allows users to authenticate using the organization's Identity Provider (IdP).
This document is divided into two main sections:
-- **[User Guide](#user-guide-configuration)**: For organization administrators to configure SAML SSO through Prowler App.
+- **[User Guide](#user-guide-configuration)**: For organization administrators to configure SAML SSO through Prowler Cloud.
-- **[Developer and Administrator Guide](#developer-and-administrator-guide)**: For developers and system administrators running self-hosted Prowler App instances, providing technical details on environment configuration, API usage, and testing.
+- **[Developer and Administrator Guide](#developer-and-administrator-guide)**: For developers and system administrators running Prowler Local Server instances, providing technical details on environment configuration, API usage, and testing.
---
@@ -44,7 +48,7 @@ If the SAML configuration is removed, users who previously authenticated via SAM
#### Step 1: Access Profile Settings
-To access the account settings, click the "Account" button in the top-right corner of Prowler App, or navigate directly to `https://cloud.prowler.com/profile` (or `http://localhost:3000/profile` for local setups).
+To access the account settings, click the "Account" button in the top-right corner of Prowler Cloud, or navigate directly to `https://cloud.prowler.com/profile` (or `http://localhost:3000/profile` for local setups).

@@ -64,12 +68,12 @@ Choose a Method:
- Prowler App displays the SAML configuration information needed to configure the IdP. Use this information to create a new SAML application in the IdP.
+ Prowler Cloud displays the SAML configuration information needed to configure the IdP. Use this information to create a new SAML application in the IdP.
1. **Assertion Consumer Service (ACS) URL**: The endpoint in Prowler that will receive the SAML assertion from the IdP.
2. **Audience URI (Entity ID)**: A unique identifier for the Prowler application (Service Provider).
- To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler App and use them to set up a new SAML application.
+ To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler Cloud and use them to set up a new SAML application.

@@ -82,15 +86,21 @@ Choose a Method:
**Configure Attribute Mapping in the IdP**
- For Prowler App to correctly identify and provision users, configure the IdP to send the following attributes in the SAML assertion:
+ For Prowler Cloud to correctly identify and provision users, configure the IdP to send the following attributes in the SAML assertion:
| Attribute Name | Description | Required |
|----------------|---------------------------------------------------------------------------------------------------------|----------|
| `firstName` | The user's first name. | Yes |
| `lastName` | The user's last name. | Yes |
- | `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler App creates a new role with that name with read-only access (visibility over all providers, no management permissions). If `userType` is not defined, the user's existing roles are left unchanged. Role permissions can be edited in the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). | No |
+ | `userType` | Determines which Prowler role the user receives (e.g., `admin`, `auditor`). If a role with that name already exists, the user receives it automatically; if it does not exist, Prowler Cloud creates a new role with that name with read-only access (visibility over all providers, no management permissions). A Prowler administrator can adjust its permissions through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac). If `userType` is not defined, Prowler Cloud applies the fallback behavior described below. | No |
| `organization` | The user's company name. | No |
+ **Fallback Role Without `userType`**
+
+
+
+ If `userType` is not defined, the user's existing roles are left unchanged. Users without an existing role in that tenant receive a least-privilege `read_only` fallback role. If `read_only` already belongs to a role with different permissions, Prowler Cloud checks suffixed names in order, starting with `read_only_0`. It reuses the first role with the fallback permissions or creates the first available name. A Prowler administrator can then assign the appropriate role through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac).
+
**IdP Attribute Mapping**
@@ -101,13 +111,13 @@ Choose a Method:
**Single-Value `userType` Required**
- Map `userType` to an IdP attribute that always contains a single value. If the IdP sends multiple values, Prowler App uses only the first value and does not assign multiple roles or select the highest-privilege role.
+ Map `userType` to an IdP attribute that always contains a single value. If the IdP sends multiple values, Prowler Cloud uses only the first value and does not assign multiple roles or select the highest-privilege role.
**Dynamic Updates**
- Prowler App updates these attributes each time a user logs in. Any changes made in the Identity Provider (IdP) will be reflected when the user logs in again.
+ Prowler Cloud updates these attributes each time a user logs in. Any changes made in the Identity Provider (IdP) will be reflected when the user logs in again.
@@ -139,31 +149,36 @@ Choose a Method:

- 7. **Configure User Attributes in Okta**: Okta acts as the central source for user profile information. Prowler App maps the following Okta user profile attributes during each SAML login:
+ 7. **Configure User Attributes in Okta**: Okta acts as the central source for user profile information. Prowler Cloud maps the following Okta user profile attributes during each SAML login:
- * **First name** (`firstName`): Maps to the user's first name in Prowler App.
- * **Last name** (`lastName`): Maps to the user's last name in Prowler App.
+ * **First name** (`firstName`): Maps to the user's first name in Prowler Cloud.
+ * **Last name** (`lastName`): Maps to the user's last name in Prowler Cloud.

- * **Organization** (`organization`): Maps to the company name displayed in Prowler App. This attribute is optional.
- * **User type** (`userType`): Determines the Prowler role assigned to the user. This attribute is **case-sensitive**: if it matches the exact name of an existing role in Prowler App the user receives that role; if no role with that name exists, a new one is created with read-only access.
+ * **Organization** (`organization`): Maps to the company name displayed in Prowler Cloud. This attribute is optional.
+ * **User type** (`userType`): Determines the Prowler role assigned to the user. This attribute is **case-sensitive**: if it matches the exact name of an existing role in Prowler Cloud the user receives that role; if no role with that name exists, a new one is created with read-only access.

- To modify these values, edit the user's profile directly in the Okta admin console under the "Profile" tab. Changes are reflected in Prowler App the next time the user logs in via SAML.
+ To modify these values, edit the user's profile directly in the Okta admin console under the "Profile" tab. Changes are reflected in Prowler Cloud the next time the user logs in via SAML.
**User Type and Role Assignment**
The `userType` attribute controls which Prowler role is assigned to the user:
- * If a role with the specified name already exists in Prowler App, the user automatically receives that role.
- * If the role does not exist, Prowler App creates a new role with that exact name with read-only access: the user can see all providers and their findings but cannot manage anything. A Prowler administrator (a user whose role includes the "Manage Account" permission) can adjust its permissions afterward through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac).
- * If `userType` is not defined in the user's Okta profile, the user's existing roles in Prowler App are left unchanged.
- * `userType` must contain a single value. If the IdP sends multiple values, Prowler App uses only the first value and does not assign multiple roles.
+ * If a role with the specified name already exists in Prowler Cloud, the user automatically receives that role.
+ * If the role does not exist, Prowler Cloud creates a new role with that exact name with read-only access: the user can see all providers and their findings but cannot manage anything. A Prowler administrator (a user whose role includes the "Manage Account" permission) can adjust its permissions afterward through the [RBAC Management tab](/user-guide/tutorials/prowler-app-rbac).
+ * `userType` must contain a single value. If the IdP sends multiple values, Prowler Cloud uses only the first value and does not assign multiple roles.
- **Example:** To assign the `IT` role to a user, set the `userType` value to `IT` in Okta. If a role named `IT` already exists in Prowler App, the user receives it automatically upon login. If it does not exist, Prowler App creates a new role called `IT` with read-only access, and a Prowler administrator can adjust its permissions as needed.
+ **Fallback Role Without `userType`**
+
+
+
+ If `userType` is not defined in the user's Okta profile, the user's existing roles in Prowler Cloud are left unchanged. Users without an existing role in that tenant receive a least-privilege `read_only` fallback role until a Prowler administrator assigns another role. If `read_only` already belongs to a role with different permissions, Prowler Cloud checks suffixed names in order, starting with `read_only_0`. It reuses the first role with the fallback permissions or creates the first available name.
+
+ **Example:** To assign the `IT` role to a user, set the `userType` value to `IT` in Okta. If a role named `IT` already exists in Prowler Cloud, the user receives it automatically upon login. If it does not exist, Prowler Cloud creates a new role called `IT` with read-only access, and a Prowler administrator can adjust its permissions as needed.
@@ -179,11 +194,11 @@ Choose a Method:
Once the IdP is configured, it provides a **metadata XML file**. This file contains the IdP's configuration information, such as its public key and login URL.
-To complete the Prowler App configuration:
+To complete the Prowler Cloud configuration:
1. Return to the Prowler SAML configuration page.
-2. Enter the **email domain** for the organization (e.g., `mycompany.com`). Prowler App uses this to identify users who should authenticate via SAML.
+2. Enter the **primary email domain** for the organization (e.g., `mycompany.com`). Prowler Cloud uses this domain to generate the Assertion Consumer Service (ACS) URL. Every configured domain can identify users who authenticate through this SAML configuration.
3. Upload the **metadata XML file** downloaded from the IdP.
@@ -191,27 +206,47 @@ To complete the Prowler App configuration:
#### Step 5: Save and Verify Configuration
-Click the "Save" button to complete the setup. The "SAML Integration" card will now display an "Active" status, indicating the configuration is complete and enabled.
+Click the "Save" button to complete the setup. The "SAML SSO Integration" card will now display an "Enabled" status, indicating the configuration is complete and enabled.
-
+
-
-**IdP Configuration**
+---
-The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application.
+### Add Multiple SAML Domains
-
+
+
+
+
+Prowler Cloud supports one primary domain and up to 19 additional verified email domains in the same SAML configuration. Users from every configured domain authenticate through the same Identity Provider (IdP), so separate SAML applications are not required for each domain.
+
+
+A SAML configuration supports up to 20 email domains in total. One domain is required as the primary domain, leaving 19 slots for additional domains. Each subdomain counts as a separate additional domain. For example, `partners.example.com` counts separately from `example.com`.
+
+
+The ACS URL always uses the primary domain. Configure this single ACS URL in the IdP even when the SAML configuration includes additional domains.
+
+To add domains to a new or existing SAML configuration:
+
+1. Enter the domain in **Additional Email Domains**.
+2. Click **Add**. Each additional domain must be unique and must differ from the primary domain.
+3. Repeat these steps for every domain that must share the configuration.
+4. Click **Save** for a new configuration or **Update** for an existing configuration.
+
+
+
+To remove an additional domain, click the remove button next to the domain, then click **Update**. Users from a removed domain can no longer start SAML authentication through this configuration.
### Remove SAML Configuration
SAML SSO can be disabled by removing the existing configuration from the integration panel.
-
+
### IdP-Initiated SSO
Once SAML SSO is configured, users can access Prowler Cloud directly from their Identity Provider's dashboard:
1. Navigate to the IdP dashboard or portal
-2. Click on the Prowler Cloud application tile
+2. Click the Prowler Cloud application tile
3. The system automatically authenticates users and redirects them to Prowler Cloud
This method is convenient for users who primarily work from the IdP portal and prefer a seamless single-click access.
@@ -222,11 +257,11 @@ Users can also initiate the login process directly from Prowler's login page:
1. Navigate to the Prowler login page
2. Click "Continue with SAML SSO"
- 
+ 
3. Enter their email address from the configured domain
- 
+ 
4. The system redirects users to the IdP for authentication
-5. After successful authentication, users are returned to Prowler App
+5. After successful authentication, users are returned to Prowler Cloud
This method is useful when users bookmark Prowler or navigate directly to the application.
@@ -234,11 +269,11 @@ This method is useful when users bookmark Prowler or navigate directly to the ap
## Developer and Administrator Guide
-This section provides technical details for developers and administrators of self-hosted Prowler instances.
+This section provides technical details for developers and administrators of Prowler Local Server instances.
### Environment Configuration
-For self-hosted deployments, several environment variables must be configured to ensure SAML SSO functions correctly. These variables are typically set in an `.env` file.
+For Prowler Local Server deployments, several environment variables must be configured to ensure SAML SSO functions correctly. These variables are typically set in an `.env` file.
| Variable | Description | Example |
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
diff --git a/docs/user-guide/tutorials/prowler-app.mdx b/docs/user-guide/tutorials/prowler-app.mdx
index 2218ae4bb7..59f6a18f94 100644
--- a/docs/user-guide/tutorials/prowler-app.mdx
+++ b/docs/user-guide/tutorials/prowler-app.mdx
@@ -7,16 +7,16 @@ import { ProviderCards } from "/snippets/provider-cards.mdx"
**Prowler Cloud** is a web application that simplifies running Prowler. This tutorial will guide you through setting up and using it.
-We refer to **Prowler App** as the self-hosted version of **Prowler Cloud**.
+**Prowler Local Server** is the self-hosted version of **Prowler Cloud**. See [Prowler product families](/getting-started/products) for every official product name.
## Accessing Prowler Cloud and API Documentation
If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user, you can access API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs)
-**For Prowler App users**
+**For Prowler Local Server users**
-After [installing](/getting-started/installation/prowler-app) **Prowler App**, access it at [http://localhost:3000](http://localhost:3000).
+After [installing](/getting-started/installation/prowler-app) **Prowler Local Server**, access it at [http://localhost:3000](http://localhost:3000).
To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses.
@@ -45,7 +45,7 @@ See [how to configure Social Login for Prowler](/user-guide/tutorials/prowler-ap
## Step 2: Log In
-Once registered, log in with your email and password to access Prowler App.
+Once registered, log in with your email and password to access Prowler Cloud.
@@ -85,12 +85,12 @@ For detailed instructions on configuring credentials for each provider, refer to
## Step 5: Test Connection
-After adding your credentials of your cloud account, click the `Launch` button to verify that Prowler App can successfully connect to your provider:
+After adding your cloud account credentials, click the `Check connection` button to verify that Prowler can successfully connect to your provider:
## Step 6: Scan Started
-After successfully adding and testing your credentials, Prowler will start scanning your cloud environment, click the `Go to Scans` button to see the progress:
+After the connection check succeeds, save the provider and start your first scan with the `Launch Scan` button. The `Scans` section shows the scan in progress:
@@ -127,28 +127,22 @@ While the scan is running, start exploring the findings in these sections:
-- **Issues**: Types of issues detected.
-
-
-
-- **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more.
+- **Findings**: Detailed list of findings detected, where you can filter by severity, service, and more.
To view all `new` findings that have not been seen prior to this scan, click the `Delta` filter and select `new`. To view all `changed` findings that have had a status change (from `PASS` to `FAIL` for example), click the `Delta` filter and select `changed`.
## Step 9: Download the Outputs
-Once a scan is complete, navigate to the Scan Jobs section to download the output files generated by Prowler:
+Once a scan is complete, navigate to the `Scans` section to download the output files generated by Prowler:
-
-
-You can download the output files generated by Prowler as a single `zip` file. This archive contains the CSV, JSON-OSCF, and HTML reports detailing the findings.
+You can download the output files generated by Prowler as a single `zip` file. This archive contains the CSV, JSON-OCSF, and HTML reports detailing the findings.
To download these files, click the **Download** button. This button becomes available only after the scan has finished.
-The `zip` file unpacks into a folder named like `prowler-output--`, which includes all of the above outputs. In the example below, you can see the `.csv`, .`json`, and `.html` reports alongside a subfolder for detailed compliance checks.
+The `zip` file unpacks into a folder named like `prowler-output--`, which includes all of the above outputs. In the example below, you can see the `.csv`, `.json`, and `.html` reports alongside a subfolder for detailed compliance checks.
@@ -163,8 +157,6 @@ Once your scan has finished, you don’t need to grab the entire ZIP—just pull
- Navigate to the **Compliance** section of the UI.
-
-
- Find the Framework report you need.
- Click its **Download** icon to retrieve that report’s CSV file with all the detailed findings.
diff --git a/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx
index f4d19fc4aa..c884e6ff6b 100644
--- a/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx
+++ b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx
@@ -8,12 +8,14 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
-Prowler Cloud enables you to onboard all AWS accounts in your Organization through a single guided wizard. Instead of connecting accounts one by one, you can discover every account in your AWS Organization, select the ones you want to monitor, test connectivity, and launch scans — all from the Prowler Cloud UI.
+Prowler Cloud onboards every AWS account in your Organization through a single guided wizard. Instead of connecting accounts one by one, you can discover every account in your AWS Organization, select the ones you want to monitor, test connectivity, and launch scans — all from the Prowler Cloud UI.
For CLI-based multi-account scanning, see [AWS Organizations in Prowler CLI](/user-guide/providers/aws/organizations).
+To follow this guide you need an active [Prowler Cloud](https://cloud.prowler.com) account and access to your AWS Organization [management account](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) (or a registered delegated administrator account).
+
## Overview
### Individual Accounts vs Organizations
@@ -25,225 +27,17 @@ For CLI-based multi-account scanning, see [AWS Organizations in Prowler CLI](/us
### How It Works
-Before using the AWS Organizations wizard, you need to deploy **two Identity and Access Management (IAM) roles** in your AWS environment. The onboarding follows this sequence:
+
+
+Onboarding deploys the **ProwlerScan Identity and Access Management (IAM) role** in your management account and in every member account. A **single CloudFormation stack** — launched from the wizard's **Create Stack in Management Account** button ([Step 2](#step-2-authenticate-with-your-management-account)) — creates the management account role **and** a service-managed StackSet that rolls the role out to your member accounts in one operation. Prefer to deploy the roles yourself? See [Deploy the Roles Manually](#deploy-the-roles-manually).
-
+
-## Key Concepts
+## Step 1: Start the Organization Wizard
-### What Is an External ID?
-
-An **External ID** is a security token that Prowler generates unique to your tenant. When Prowler assumes the IAM role in your AWS account, it presents this External ID to prove its identity.
-
-This prevents the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) — a scenario where an unauthorized party could trick AWS into granting access to your account. By requiring the External ID, only your specific Prowler tenant can assume the role.
-
-You don't need to create the External ID yourself — Prowler generates it automatically and displays it in the wizard for you to copy.
-
-### Two Roles Architecture
-
-Prowler requires **two separate IAM roles** deployed in different places, each with a distinct purpose:
-
-| Role | Where it lives | What it does | How to deploy it |
-|------|---------------|--------------|------------------|
-| **ProwlerScan** (management account) | Your management (root) account only | Discovers the Organization structure **and** scans the management account. Has additional Organizations discovery permissions. | Via **Quick Create** link or **manually** in the IAM Console ([Step 1](#step-1-create-the-management-account-role)). Cannot be deployed via StackSet. |
-| **ProwlerScan** (member accounts) | Every member account | Scans the account for security findings. | Via **CloudFormation StackSet** ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Automated across all accounts. |
-
-
-
-
-
-
-**Same name, different permissions.** Both roles are named `ProwlerScan` — Prowler expects a consistent role name across all accounts. The management account role has the same scanning permissions as member accounts, plus additional Organizations discovery permissions (see [Step 1](#step-1-create-the-management-account-role) for the full list).
-
-
-### What Is a CloudFormation StackSet?
-
-A [CloudFormation StackSet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html) lets you deploy the same CloudFormation template across multiple AWS accounts in a single operation. Prowler uses a StackSet to deploy the **ProwlerScan** IAM role into every member account of your organization, so you don't have to create the role manually in each account.
-
-## Prerequisites
-
-### Prowler Cloud Account
-
-You need an active [Prowler Cloud](https://cloud.prowler.com) account. Each AWS account you connect will count as a provider in your subscription. See [Billing Impact](#billing-impact) for details.
-
-### AWS Organization Enabled
-
-Your AWS environment must have [AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) enabled. You will need access to the **management account** (or a delegated administrator account) to provide the Organization ID and IAM Role ARN.
-
-## Step 1: Create the Management Account Role
-
-The first role you need to create is the **management account role**. This role allows Prowler to discover your Organization structure — listing accounts, OUs, and hierarchy.
-
-
-**StackSets do not deploy to the management account.** Organizational CloudFormation StackSets with service-managed permissions only target member accounts — this is an AWS limitation, not a Prowler one. You must create the management account role separately, either via the Quick Create link ([Option A](#option-a-quick-create-link-fastest)) or manually ([Option B](#option-b-create-the-role-manually)).
-
-
-
-**The role must be named `ProwlerScan`** — the same name as the role deployed to member accounts via StackSet. Prowler expects a consistent role name across all accounts in the Organization. If you use a different name, connection tests and scans will fail for the management account.
-
-
-### Option A: Quick Create Link (Fastest)
-
-The Prowler wizard provides a one-click link that opens the AWS Console with the CloudFormation template pre-configured. This creates a **CloudFormation Stack** (not a StackSet) that deploys the ProwlerScan role with Organizations permissions enabled in your management account.
-
-
-**[Open Quick Create Stack in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler¶m_EnableOrganizations=true)**
-
-Opens the CloudFormation Console with the Prowler scan role template and `EnableOrganizations=true` pre-filled. You will need to enter the **ExternalId** parameter manually — copy it from the Prowler wizard ([Step 4](#step-4-authenticate-with-your-management-account)).
-
-
-1. Click **[Open Quick Create Stack in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler¶m_EnableOrganizations=true)** or use the **Create Stack in Management Account** button in the Prowler wizard (which also pre-fills the ExternalId).
-2. Enter the **ExternalId** parameter if not pre-filled.
-3. Check **"I acknowledge that AWS CloudFormation might create IAM resources with custom names"** and click **Create stack**.
-4. Wait for the stack to reach **CREATE_COMPLETE** status.
-
-Take note of the **Role ARN** from the stack's **Outputs** tab — you will need it in the wizard.
-
-### Option B: Create the Role Manually
-
-1. Sign in to the [AWS IAM Console](https://console.aws.amazon.com/iam/) in your **management account**.
-
-2. Go to **Roles > Create role** and select **Custom trust policy**.
-
-3. Paste the following trust policy. This allows Prowler Cloud to assume the role using your tenant's External ID (you will get this from the Prowler wizard in [Step 3](#step-3-start-the-organization-wizard)):
-
-```json
-{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Principal": {
- "AWS": "arn:aws:iam::232136659152:root"
- },
- "Action": "sts:AssumeRole",
- "Condition": {
- "StringEquals": {
- "sts:ExternalId": ""
- },
- "StringLike": {
- "aws:PrincipalArn": "arn:aws:iam::232136659152:role/prowler*"
- }
- }
- }
- ]
-}
-```
-
-Replace `` with the External ID shown in the Prowler wizard.
-
-4. Attach the following AWS managed policies:
- - **SecurityAudit**
- - **ViewOnlyAccess**
-
- This allows Prowler to also scan the management account for security findings, just like any other account.
-
-5. Create an additional inline policy with the following permissions. These are specific to the management account and allow Prowler to discover your Organization structure:
-
-```json
-{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Sid": "ProwlerOrganizationDiscovery",
- "Effect": "Allow",
- "Action": [
- "organizations:DescribeAccount",
- "organizations:DescribeOrganization",
- "organizations:ListAccounts",
- "organizations:ListAccountsForParent",
- "organizations:ListOrganizationalUnitsForParent",
- "organizations:ListRoots",
- "organizations:ListTagsForResource"
- ],
- "Resource": "*"
- },
- {
- "Sid": "ProwlerStackSetManagement",
- "Effect": "Allow",
- "Action": [
- "organizations:RegisterDelegatedAdministrator",
- "iam:CreateServiceLinkedRole"
- ],
- "Resource": "*"
- }
- ]
-}
-```
-
-
-You can optionally restrict the `Resource` field to your specific Organization ARN (e.g., `arn:aws:organizations::123456789012:organization/o-abc123def4`) instead of `"*"` to minimize the blast radius.
-
-
-6. Name the role **`ProwlerScan`** and click **Create role**. Take note of the **Role ARN** — you will need it in the Prowler wizard.
-
-The ARN follows this format: `arn:aws:iam:::role/ProwlerScan`
-
-
-The role **must** be named `ProwlerScan`. Do not use a different name.
-
-
-
-If you just created the role, it may take up to **60 seconds** for AWS to propagate it. If you get an error in the Prowler wizard, wait a moment and try again.
-
-
-## Step 2: Deploy the CloudFormation StackSet
-
-After creating the management account role, the next step is to deploy the **ProwlerScan** role to your member accounts using a CloudFormation StackSet. This is the recommended method for consistent, scalable deployment across your entire organization.
-
-The StackSet uses **service-managed permissions**, which means AWS Organizations handles the cross-account deployment automatically — you don't need to create execution roles manually in each account. The StackSet deploys the ProwlerScan IAM role in every target member account, enabling Prowler to assume that role for cross-account scanning.
-
-
-**Trusted access required:** CloudFormation StackSets must have trusted access enabled in your management account. Verify this in the AWS Console under **AWS Organizations > Settings > Trusted access for AWS CloudFormation StackSets**.
-
-
-
-**The Quick Create link creates a Stack, not a StackSet.** The link in the Prowler wizard creates a CloudFormation **Stack** that deploys the ProwlerScan role in your management account only ([Step 1](#step-1-create-the-management-account-role)). To deploy the role across **member accounts**, you must create a StackSet manually as described below. AWS does not support Quick Create links for StackSets.
-
-
-
-**[Open StackSets Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create)**
-
-Opens the CloudFormation StackSets creation page directly. You will need to paste the template URL and ExternalId manually.
-
-
-1. Click the link above or navigate to **CloudFormation > StackSets > Create StackSet** in your management account.
-2. Choose **Service-managed permissions**.
-3. Select **Amazon S3 URL** as the template source and paste the following URL:
- ```
- https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml
- ```
-4. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
-5. Choose your deployment targets (entire organization or specific OUs).
-6. Select the AWS regions where you want the role deployed.
-7. Click **Create StackSet**.
-
-### Verify StackSet Deployment
-
-After deploying, verify that all stack instances completed successfully:
-
-1. In the CloudFormation Console, go to **StackSets** and select your Prowler StackSet.
-2. Click the **Stack instances** tab.
-3. Confirm that all instances show **Status: CURRENT** and **Stack status: CREATE_COMPLETE**.
-
-Deployment typically takes **2–5 minutes** for medium-sized organizations. Large organizations (500+ accounts) may take longer.
-
-
-**Prefer Terraform?** You can deploy the ProwlerScan role using Terraform instead. See the [StackSets deployment guide](/user-guide/providers/aws/organizations#deploying-prowler-iam-roles-across-aws-organizations) for the Terraform module.
-
-
-### Key Considerations
-
-- **Service-managed permissions**: Always select **Service-managed permissions** when creating the StackSet. This lets AWS Organizations manage the deployment automatically across current and future member accounts.
-- **Least privilege**: The ProwlerScan role deployed by the StackSet uses `SecurityAudit` and `ViewOnlyAccess` — AWS managed policies that grant read-only access — plus a small set of additional read-only permissions for services not covered by those policies. See the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) for the full list. Prowler does not make any changes to your accounts.
-- **New accounts**: When you add new accounts to your AWS Organization, the StackSet automatically deploys the ProwlerScan role to them if you targeted the organization root or the relevant OU. Combined with Prowler's 6-hour automatic sync, new accounts are onboarded end-to-end without manual intervention.
-- **Management account**: Organizational StackSets **do not deploy to the management account itself**. If you want to scan the management account, you need to create the ProwlerScan role there separately using a regular CloudFormation Stack.
-
-## Step 3: Start the Organization Wizard
-
-Now that both roles are deployed — the management account role (Step 1) and the ProwlerScan role in member accounts (Step 2) — you can start the Prowler wizard.
+The Prowler wizard walks you through the entire flow: deploying both roles from a single CloudFormation stack, discovering your accounts, testing connectivity, and launching scans.
### Open the Wizard
@@ -280,29 +74,50 @@ Now that both roles are deployed — the management account role (Step 1) and th
Click **Next** to proceed to the authentication phase.
-## Step 4: Authenticate with Your Management Account
+## Step 2: Authenticate with Your Management Account
-The wizard's **Authentication Details** page guides you through three actions: deploying the roles in AWS, entering the management account Role ARN, and confirming the deployment.
+The **Authentication Details** page guides you through three actions: deploying the roles in AWS, entering the deployment account Role ARN, and confirming the deployment. The deployment account is either the management account or, when delegated administrator mode is selected, the delegated administrator account.
### External ID
-The wizard displays a **Prowler External ID** at the top — auto-generated and unique to your tenant. Click the copy icon to copy it. You will need this External ID for both the management account Stack and the member accounts StackSet.
+The wizard displays a **Prowler External ID** at the top — auto-generated and unique to your tenant. Click the copy icon to copy it. The External ID is pre-filled into the deployment link, and the single stack applies it to both the management account role and the member-account StackSet. Learn more in [What Is an External ID?](#what-is-an-external-id).
### Deploy the Roles
-The wizard provides two deployment actions:
+
-1. **Create Stack in Management Account** — opens a Quick Create link that deploys the ProwlerScan role with `EnableOrganizations=true` in your management account ([Step 1](#step-1-create-the-management-account-role)). The External ID is pre-filled.
+The wizard deploys the deployment account role and the member-account StackSet in a **single** CloudFormation Stack:
-2. **Open StackSets Console** — links to the CloudFormation StackSets console where you create a StackSet for member accounts ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Copy the template URL shown in the wizard and paste the External ID manually.
+
+**Prefer to use your own role?** You do not have to use the Quick Create template. Create the ProwlerScan role yourself — through the IAM Console, Terraform, or your own CloudFormation [(Following this guide)](#deploy-the-roles-manually) — and paste its ARN into the Role ARN field below. The role must use the external ID from the earlier step and include the trust policy and permissions described in [Deploy the Roles Manually](#deploy-the-roles-manually).
+
+
+1. **Organizational Unit or Root ID** — enter the AWS OU (`ou-xxxx-yyyyyyyy`) or organization root (`r-xxxx`) you want to onboard. Prowler rolls the ProwlerScan role out to every member account under this target. Find it in the [AWS Organizations Console](https://console.aws.amazon.com/organizations/); use the **root ID** (`r-`) to cover the entire organization or an **OU ID** (`ou-`) to target a specific unit.
+
+2. *(Optional)* Check **"I'm deploying from a delegated administrator account"** if you launch the stack from a delegated administrator account instead of the management account.
+
+3. **Create Stack in Management Account** — or **Create Stack in Delegated Administrator Account** when delegated administrator mode is selected — opens a Quick Create link that deploys, in a single stack: the ProwlerScan role in the account where you launch the stack (`DeployLocalRole`, with `EnableOrganizations=true`) **and** a service-managed StackSet (`DeployStackSet`) that rolls the role out to your member accounts. The External ID, OU/Root ID, and deployment options are pre-filled.
-
+
-### Enter the Management Account Role ARN
+
+**Finding your Organizational Unit or Root ID.** In the [AWS Organizations Console](https://console.aws.amazon.com/organizations/) the root (`r-…`) and OU (`ou-…`) IDs appear in the account tree, or run these from your management account:
-Paste the **Role ARN** of the management account role you created in [Step 1](#step-1-create-the-management-account-role) into the **Management Account Role ARN** field.
+```bash
+# Root ID — deploys the role to the entire organization
+aws organizations list-roots --query 'Roots[0].Id' --output text
+
+# OU IDs under the root — to target a specific unit instead
+aws organizations list-organizational-units-for-parent --parent-id r-xxxx \
+ --query 'OrganizationalUnits[].{Name:Name,Id:Id}' --output table
+```
+
+
+### Enter the Deployment Account Role ARN
+
+Paste the **Role ARN** created by the stack above into the **Management Account Role ARN** field or, when delegated administrator mode is selected, the **Delegated Administrator Account Role ARN** field.
The ARN follows this format:
```
@@ -312,12 +127,16 @@ arn:aws:iam:::role/ProwlerScan
For example: `arn:aws:iam::123456789012:role/ProwlerScan`
-
+
+
+It may take up to **60 seconds** for AWS to generate the IAM Role ARN after the stack completes. If the wizard reports an error, wait a moment and try again.
+
+
### Confirm and Discover
-1. Check the box: **"The Stack and StackSet have been successfully deployed in AWS"**.
+1. Check the box: **"The Stack has been successfully deployed in AWS"**.
2. Click **Authenticate**.
Here's what happens behind the scenes:
@@ -325,7 +144,22 @@ Here's what happens behind the scenes:
- An asynchronous discovery is triggered to query your AWS Organization structure.
- You will see a **"Gathering AWS Accounts..."** spinner — this typically takes **30 seconds to 2 minutes** depending on your organization size.
-## Step 5: Select Accounts to Scan
+#### When Discovery Takes Too Long
+
+
+
+Prowler waits up to **3 minutes** for a result. Past that, the wizard stops waiting — but the discovery keeps running in the background — and offers two actions:
+
+- **Keep waiting** — resume the same discovery. Nothing is re-read from AWS.
+- **Retry** — start a fresh discovery, which queries your Organization structure again.
+
+
+
+
+
+If discovery fails outright, the wizard reports the error and offers **Retry discovery**.
+
+## Step 3: Select Accounts to Scan
### Understanding the Tree View
@@ -336,6 +170,7 @@ Once discovery completes, the wizard displays a **hierarchical tree view** of yo
- The tree supports up to **5 levels of nesting** (Root > OUs > Sub-OUs > Accounts).
+- If you deployed the stack for just one OU, that OU will be preselected in the tree.
- **Selecting an OU** automatically selects all accounts within it.
- **Individual overrides**: deselect specific accounts even if the parent OU is selected.
- The header shows **"X of Y accounts selected"** to track your selection.
@@ -352,14 +187,12 @@ Only **ACTIVE** accounts can be selected for scanning:
| **CLOSED** | No | Account has been closed. |
-**Your existing data is safe.** If an AWS account is already connected to Prowler as an individual provider, it will appear in the tree with a checkmark indicator.
+**Your existing data is safe.** If an AWS account is already connected to Prowler as an individual provider, it appears in the tree with a checkmark indicator.
When you proceed:
- The existing provider is **linked** to the organization — it is **not** duplicated.
- All your **historical scan data and findings are preserved** — nothing is overwritten.
- There is **no additional billing** — the existing provider is reused.
-
-This is completely safe. You are simply associating the account with the organization for easier management.
### Custom Aliases
@@ -368,14 +201,26 @@ You can edit the display name for each account before connecting. This alias is
### Blocked Accounts
-Some accounts may appear as **blocked** (grayed out, not selectable). This happens when:
-- The account is **already linked to a different organization** in Prowler (`linked_to_other_organization`).
+Some accounts appear as **blocked** (grayed out, not selectable) when onboarding them would conflict with something Prowler already stores. Hover over the blocked account to see the specific reason.
-Hover over the blocked account to see the specific reason.
+| Reason | What it means |
+|--------|---------------|
+| `organization_conflict` | The account is already connected under a **different** Prowler organization. |
+| `organization_node_conflict` | The account is already grouped under a different organizational unit in Prowler — for example, it moved in AWS after it was onboarded. |
-## Step 6: Test Connections
+### Accounts That Already Have Credentials
-### How Connection Testing Works
+
+
+Applying your selection stores the organization credential on every selected account. When a selected account is already connected to Prowler with its own credential, that credential is **overwritten** — so the wizard asks first, naming the affected accounts:
+
+
+
+
+
+Click **Replace and continue** to proceed, or **Cancel** to adjust your selection. Historical scans and findings are preserved either way — only the credential changes.
+
+## Step 4: Test Connections
Click **Test Connections** to verify that Prowler can assume the **ProwlerScan** role in each selected member account.
@@ -383,154 +228,273 @@ Click **Test Connections** to verify that Prowler can assume the **ProwlerScan**
-- Each account shows a real-time status indicator:
- - **Spinner** — test in progress
- - **Green checkmark (✓)** — connection successful
- - **Red icon (✗)** — connection failed (hover to see the error)
-
-### All Tests Pass
+Each account shows a real-time status indicator:
+- **Spinner** — test in progress
+- **Green checkmark (✓)** — connection successful
+- **Red icon (✗)** — connection failed (hover to see the error)
If every account connects successfully, you automatically advance to the next step.
-### Some Tests Fail
+### When Some Tests Fail
-An error banner appears: **"There was a problem connecting to some accounts."**
-
-You have two options:
+An error banner appears: **"There was a problem connecting to some accounts. Hover each account to check the error."** You have two options:
**a) Fix and retry:**
1. Go to the AWS Console and verify the StackSet deployed to the failing accounts.
2. Check that the External ID in the StackSet matches the one shown in Prowler.
3. Return to Prowler and click **Test Connections** — only the **failed accounts are re-tested** (smart retry). Accounts that already passed are not tested again.
-
-
-
-
**b) Skip and continue:**
-Click **Skip Connection Validation** to proceed with only the accounts that connected successfully. The failed accounts will not be scanned.
+Click **Skip Connection Validation** to proceed with only the accounts that connected successfully. The failed accounts will not be scanned. This option is only available when at least one account connected successfully.
-
-**Skip Connection Validation** is only available when at least one account connected successfully.
-
+If **no accounts** connected successfully, the banner instead reads *"No accounts connected successfully. Fix the connection errors and retry before launching scans."* and you cannot proceed. Fix the underlying connection issues — see [Troubleshooting](#troubleshooting) — and retry before launching scans.
-### All Tests Fail
-
-If **no accounts** connected successfully, you cannot proceed:
-
-> *"No accounts connected successfully. Fix the connection errors and retry before launching scans."*
-
-You must fix the underlying connection issues before continuing. See [Updating Credentials](#updating-credentials) below.
-
-### Updating Credentials
-
-If connection tests fail, here's how to fix common issues:
-
-1. Open the [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) and check that your StackSet instances show **CREATE_COMPLETE** for the failing accounts. If not, update the StackSet to include the missing OUs.
-2. Compare the **ExternalId** parameter in your StackSet with the External ID displayed in the Prowler wizard. They must match exactly.
-3. After fixing the issue in AWS, return to Prowler and click **Test Connections**. Only the previously failed accounts will be re-tested.
-
-## Step 7: Launch Scans
-
-### Choose Scan Schedule
+## Step 5: Launch Scans
The Organizations wizard uses the same schedule controls described in [Scan Scheduling](/user-guide/tutorials/prowler-scan-scheduling#schedule-options).
-### Launch
-
-Click **Save**, **Save and launch scan**, or **Launch scan**, depending on the selected schedule option. A toast notification confirms whether the schedule was saved, scans were launched, or both. The toast includes a link to the **Scans** page. Prowler redirects to the **Providers** page.
-
-Scans are only launched for accounts that are accessible (passed connection testing) and were selected.
+Click **Save**, **Save and launch scan**, or **Launch scan**, depending on the selected schedule option. A toast notification confirms whether the schedule was saved, scans were launched, or both, and includes a link to the **Scans** page. Prowler then redirects to the **Providers** page. Scans launch only for accounts that passed connection testing and were selected.
-### What Happens Next
-
+After launching:
- Scans appear in the **Scans** page as they start and complete.
- Results populate the **Overview** and **Findings** pages.
-- Prowler runs an **automatic sync every 6 hours** to detect new accounts added to your Organization or accounts that have been removed. New accounts are onboarded automatically based on the parent OU configuration.
+- Prowler runs an **automatic sync every 6 hours** to detect accounts added to or removed from your Organization. New accounts under the targeted OU or root are onboarded automatically.
+
+## Manage Your Organization After Onboarding
+
+
+
+Open the row actions menu on the organization row on the **Providers** page.
+
+
+
+
+
+| Action | What it does |
+|--------|--------------|
+| **Edit Organization Name** | Renames the organization in Prowler. Leave it blank to fall back to the name stored in AWS. |
+| **Update Credentials** | Reopens the Authentication Details step to store a new Role ARN. |
+| **Edit Scan Schedule** | Applies one schedule to every connected account in the organization. |
+| **Test Connections (N)** | Re-tests every account in the organization. |
+| **Delete Organization** | Deletes the organization and cascades to its providers. |
+
+Organizational unit rows carry the same **Test Connections** and **Delete Organizational Unit** actions, scoped to the accounts beneath them.
+
+### Update Organization Credentials
+
+Choosing **Update Credentials** re-enters the Authentication Details step. Because the organization already holds a credential, Prowler warns before overwriting it and names how many providers re-authenticate with the new one:
+
+
+
+
+
+Storing a new credential runs a fresh discovery, so any discovery already in progress is discarded — discovery authenticates with the credential it started from.
+
+### Delete an Organization or Organizational Unit
+
+Deleting an organization or an organizational unit **cascades to every provider grouped under it**, along with their scans and findings. Both dialogs state how many providers are affected before you confirm.
+
+
+
+
+
+Deletion runs in the background. Prowler confirms with a **"Deletion started"** notification; if any part of it fails, the affected rows reappear on a later refresh.
+
+
+Deleting an organization **permanently deletes every account provider grouped under it**, including their historical scans and findings. This action cannot be undone.
+
+
+### When Grouping Is Unavailable
+
+If Prowler cannot read your hierarchy while loading the Providers page, a notice reads *"Organization grouping is incomplete. Some providers may appear ungrouped."* Your providers are still listed, just flat. Reload the page to try again.
## Billing Impact
Each AWS account you connect through the Organizations wizard counts as one **provider** in your Prowler Cloud subscription.
- **Already-connected accounts**: if an account was already linked as a provider, adding it to the organization does **not** incur additional billing. The existing provider is reused.
-- **Large organizations**: connecting a 500-account organization will result in up to 500 providers on your subscription. Review your plan limits before proceeding.
+- **Large organizations**: connecting a 500-account organization results in up to 500 providers on your subscription. Review your plan limits before proceeding.
- **Deleted providers**: if you later remove an account, the deleted provider no longer counts toward your subscription.
-For pricing details, see [Prowler Cloud Pricing](/getting-started/products/prowler-cloud-pricing).
+For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
## Troubleshooting
-### Invalid AWS Organization ID
+### Only Some Accounts Connect
-*"Must be a valid AWS Organization ID"*
+Discovery succeeds and the tree view appears, but only one account — or a handful — passes the connection test. This almost always means the ProwlerScan role reached the deployment account but not every member account.
-- Verify the Organization ID format: `o-` followed by 10–32 lowercase alphanumeric characters (e.g., `o-abc123def4`)
-- Copy it directly from the [AWS Organizations Console](https://console.aws.amazon.com/organizations/) to avoid typos
+- **Confirm the StackSet deployed.** Open the [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) in the deployment account, select your Prowler StackSet, open the **Stack instances** tab, and confirm every instance shows **Status: CURRENT** and **Stack status: CREATE_COMPLETE**. Instances still in progress or in a failed state explain the missing accounts.
+- **Check the targeted OU or root.** The single stack only rolls the role out to accounts under the **Organizational Unit or Root ID** you entered in [Step 2](#step-2-authenticate-with-your-management-account). Accounts in other OUs are not covered — redeploy targeting the organization root (`r-`) or add the missing OUs.
+- **Verify the deployment account.** The role is created only in the account where you launched the stack. If you deployed from a **delegated administrator account**, confirm that account is a **registered delegated administrator** for CloudFormation StackSets (registered through AWS Organizations), not just a regular member account. A regular member account cannot create a service-managed StackSet, so only its own role is created — leaving every other account without the role.
+- **Suspended accounts** cannot be scanned. Deselect them and proceed.
-### Invalid IAM Role ARN
+### No Accounts Connect
-*"Must be a valid IAM Role ARN"*
+No account passes the connection test.
-- Verify the ARN format: `arn:aws:iam::<12-digit-account-id>:role/`
-- Copy the ARN directly from the [IAM Console](https://console.aws.amazon.com/iam/) in your management account
+- **External ID mismatch.** Compare the **ExternalId** parameter in your StackSet with the External ID shown in the Prowler wizard. They must match exactly.
+- **StackSet not deployed.** Confirm the StackSet exists and its instances reached **CREATE_COMPLETE**. If you deployed the roles manually, verify [trusted access for CloudFormation StackSets](#member-account-role-stackset) is enabled.
+- **IP-based policies.** If your accounts restrict access by IP, allow the [Prowler Cloud egress IPs](/security/networking).
-### Authentication Failed
+### Authentication Fails or Times Out
-*"Authentication failed. Please verify the StackSet deployment and Role ARN"*
+*"Authentication failed. Please verify the StackSet deployment and Role ARN"* or *"Authentication timed out"*
-- Verify the management account role exists and was created in [Step 1](#step-1-create-the-management-account-role)
-- Confirm the trust policy includes the correct External ID from the wizard
-- Check the role has all Organizations discovery permissions listed in [Step 1](#step-1-create-the-management-account-role)
-- Double-check the Role ARN format and account ID for typos
+- Verify the deployment account role exists and is named exactly `ProwlerScan`.
+- Confirm the trust policy includes the correct External ID from the wizard.
+- Check the role has the Organizations discovery permissions listed in [Deploy the Roles Manually](#management-account-role).
+- Double-check the Role ARN format and account ID for typos.
+- Retry — the role can take up to **60 seconds** to propagate, and a second attempt often succeeds. For very large organizations (500+ accounts), allow extra time for discovery.
-### Authentication Timed Out
+### Invalid Organization ID or Role ARN
-*"Authentication timed out"*
+*"Must be a valid AWS Organization ID"* or *"Must be a valid IAM Role ARN"*
-- Retry the authentication step — the second attempt often succeeds
-- Check for AWS API rate limiting on the Organizations service
-- For very large organizations (500+ accounts), allow extra time for discovery
-
-### Connection Test Fails for All Accounts
-
-No accounts pass the connection test.
-
-- Verify the CloudFormation StackSet was deployed — complete [Step 2](#step-2-deploy-the-cloudformation-stackset) and wait for stack instances to reach **CREATE_COMPLETE**
-- Check that the **ExternalId** parameter in the StackSet matches the External ID shown in the Prowler wizard
-- If your accounts use IP-based IAM policies, allow [Prowler Cloud public IPs](/user-guide/tutorials/prowler-cloud-public-ips)
-
-### Connection Test Fails for Some Accounts
-
-Some accounts show a red icon while others pass.
-
-- Expand the StackSet deployment to include the OUs containing the failing accounts
-- Suspended accounts cannot be scanned — deselect them and proceed
-- Ensure the [STS regional endpoint](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) is enabled in the account's region
-- After fixing, click **Test Connections** — only the failed accounts will be re-tested
-
-### No Accounts Connected Successfully
-
-*"No accounts connected successfully. Fix the connection errors and retry before launching scans."*
-
-- Hover over the red icon on each account to see the specific error
-- Fix the underlying issues using the guidance above
-- Click **Test Connections** to retry
+- Organization ID format: `o-` followed by 10–32 lowercase alphanumeric characters (e.g., `o-abc123def4`).
+- Role ARN format: `arn:aws:iam::<12-digit-account-id>:role/ProwlerScan`.
+- Copy both directly from the AWS Console to avoid typos.
### Failed to Apply Discovery
*"Failed to apply discovery"*
-- Check the `blocked_reasons` field for any blocked accounts
-- Retry the operation
-- If the error persists, contact [Prowler Support](mailto:support@prowler.com)
+- Check the `blocked_reasons` field for any blocked accounts and retry the operation.
+- If the error persists, contact [Prowler Support](mailto:support@prowler.com).
+
+## Deploy the Roles Manually
+
+The wizard's **Create Stack** button is the fastest path, but you can create both roles yourself — for example with Terraform or your own CloudFormation — and paste the management account Role ARN into [Step 2](#step-2-authenticate-with-your-management-account). Both roles must be named `ProwlerScan`, since Prowler expects a consistent role name across all accounts.
+
+
+**Prefer Terraform?** You can deploy the ProwlerScan role across the organization with Terraform instead of CloudFormation. See the [StackSets deployment guide](/user-guide/providers/aws/organizations#deploying-prowler-iam-roles-across-aws-organizations) for the module.
+
+
+### Management Account Role
+
+The management account role lets Prowler discover your Organization structure — listing accounts, OUs, and hierarchy — and scan the management account itself. StackSets with service-managed permissions do not deploy to the management account, so this role is always created separately from the member-account StackSet.
+
+1. Sign in to the [AWS IAM Console](https://console.aws.amazon.com/iam/) in your **management account** (or delegated administrator account).
+2. Go to **Roles > Create role** and select **Custom trust policy**.
+3. Paste the following trust policy, replacing `` with the External ID shown in the Prowler wizard:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::232136659152:root"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ""
+ },
+ "StringLike": {
+ "aws:PrincipalArn": "arn:aws:iam::232136659152:role/prowler*"
+ }
+ }
+ }
+ ]
+}
+```
+
+4. Attach the AWS managed policies **SecurityAudit** and **ViewOnlyAccess** so Prowler can scan the management account for security findings.
+5. Add an inline policy with the Organizations discovery permissions:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ProwlerOrganizationDiscovery",
+ "Effect": "Allow",
+ "Action": [
+ "organizations:DescribeAccount",
+ "organizations:DescribeOrganization",
+ "organizations:ListAccounts",
+ "organizations:ListAccountsForParent",
+ "organizations:ListOrganizationalUnitsForParent",
+ "organizations:ListRoots",
+ "organizations:ListTagsForResource"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "ProwlerStackSetManagement",
+ "Effect": "Allow",
+ "Action": [
+ "organizations:RegisterDelegatedAdministrator",
+ "iam:CreateServiceLinkedRole"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+```
+
+
+You can restrict the `Resource` field to your specific Organization ARN (e.g., `arn:aws:organizations::123456789012:organization/o-abc123def4`) instead of `"*"` to minimize the blast radius.
+
+
+6. Name the role **`ProwlerScan`** and click **Create role**. The ARN follows the format `arn:aws:iam:::role/ProwlerScan` — paste it into the wizard.
+
+### Member Account Role (StackSet)
+
+Deploy the ProwlerScan role to every member account with a [CloudFormation StackSet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html), so you don't create the role manually in each account.
+
+
+**Trusted access required.** CloudFormation StackSets must have trusted access enabled in your management account. Verify this under **AWS Organizations > Settings > Trusted access for AWS CloudFormation StackSets**.
+
+
+1. In your management account, navigate to **CloudFormation > StackSets > Create StackSet** ([open directly](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create)).
+2. Choose **Service-managed permissions** so AWS Organizations deploys the role automatically across current and future member accounts.
+3. Select **Amazon S3 URL** as the template source and paste:
+ ```
+ https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml
+ ```
+4. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
+5. Choose your deployment targets (entire organization or specific OUs) and regions, then click **Create StackSet**.
+6. Open the **Stack instances** tab and confirm every instance shows **Status: CURRENT** and **Stack status: CREATE_COMPLETE**. Deployment typically takes **2–5 minutes**; large organizations (500+ accounts) may take longer.
+
+The StackSet role uses read-only access only (`SecurityAudit`, `ViewOnlyAccess`, plus a small set of additional read-only permissions). Prowler makes no changes to your accounts. See the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) for the full list. When you add new accounts under the targeted OU or root, the StackSet deploys the role automatically, and Prowler's 6-hour sync onboards them end-to-end.
+
+## Key Concepts
+
+### What Is an External ID?
+
+An **External ID** is a security token that Prowler generates unique to your tenant. When Prowler assumes the IAM role in your AWS account, it presents this External ID to prove its identity.
+
+This prevents the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) — a scenario where an unauthorized party could trick AWS into granting access to your account. By requiring the External ID, only your specific Prowler tenant can assume the role. Prowler generates it automatically and displays it in the wizard for you to copy.
+
+### Two Roles Architecture
+
+Prowler uses **two IAM roles**, both named `ProwlerScan` but deployed in different places:
+
+| Role | Where it lives | What it does |
+|------|---------------|--------------|
+| **ProwlerScan** (management account) | Your management (or delegated administrator) account | Discovers the Organization structure **and** scans that account. Includes additional Organizations discovery permissions. |
+| **ProwlerScan** (member accounts) | Every member account | Scans the account for security findings. |
+
+Both roles share the name `ProwlerScan` because Prowler expects a consistent role name across all accounts. The single CloudFormation stack in [Step 2](#step-2-authenticate-with-your-management-account) deploys both at once.
+
+
+
+
+
+### What Is a CloudFormation StackSet?
+
+A [CloudFormation StackSet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html) deploys the same CloudFormation template across multiple AWS accounts in a single operation. Prowler uses a service-managed StackSet to deploy the **ProwlerScan** IAM role into every member account of your organization, so you don't create the role manually in each account. StackSets do not deploy to the management account, which is why that role is created separately.
## What's Next
diff --git a/docs/user-guide/tutorials/prowler-cloud-azure-management-groups.mdx b/docs/user-guide/tutorials/prowler-cloud-azure-management-groups.mdx
new file mode 100644
index 0000000000..98cfe095ce
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-cloud-azure-management-groups.mdx
@@ -0,0 +1,11 @@
+---
+title: 'Azure Management Groups'
+description: 'Onboard all Azure subscriptions in your management groups through a single guided wizard'
+tag: "Coming Soon"
+---
+
+Onboarding Azure management groups through a single guided wizard is coming soon to Prowler Cloud.
+
+Today, Azure subscriptions are onboarded individually. See [Getting Started with Azure](/user-guide/providers/azure/getting-started-azure) and [Bulk Provider Provisioning](/user-guide/tutorials/bulk-provider-provisioning) to automate onboarding multiple subscriptions.
+
+Keep an eye on the [changelog](https://github.com/prowler-cloud/prowler/releases) for updates.
diff --git a/docs/user-guide/tutorials/prowler-cloud-gcp-organizations.mdx b/docs/user-guide/tutorials/prowler-cloud-gcp-organizations.mdx
new file mode 100644
index 0000000000..b7cef38c62
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-cloud-gcp-organizations.mdx
@@ -0,0 +1,442 @@
+---
+title: 'GCP Organizations'
+description: 'Onboard all GCP projects in your organization through a single guided wizard'
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
+
+
+
+Prowler Cloud onboards every Google Cloud project in your organization through a single guided wizard. Instead of connecting projects one by one, you can discover every folder and project under your Google Cloud organization, select the ones you want to monitor, test connectivity, and launch scans — all from the Prowler Cloud UI.
+
+
+For Command-Line Interface (CLI) scanning of a whole organization, see [Scanning a Specific GCP Organization](/user-guide/providers/gcp/organization).
+
+
+To follow this guide you need an active [Prowler Cloud](https://cloud.prowler.com) account and a Google Cloud credential with read access granted **at the organization node**.
+
+## Overview
+
+### Individual Projects vs Organizations
+
+| Approach | Best for | How it works |
+|----------|----------|--------------|
+| **Individual projects** | A few Google Cloud projects | Connect each project one by one with its own credential. |
+| **GCP Organizations** | 10+ projects, or any organization-managed estate | Connect once with an organization-level credential, discover every folder and project automatically, and scan them in bulk. |
+
+### How It Works
+
+Onboarding runs in four stages:
+
+1. **Grant read access** to one credential at your organization node, and enable the Cloud Resource Manager Application Programming Interface (API).
+2. **Discover** — Prowler walks your hierarchy through the Cloud Resource Manager API and returns every active folder and project.
+3. **Select and connect** — choose the projects to monitor. Prowler creates one provider per project and tests every connection.
+4. **Launch scans** — apply a scan schedule across the connected projects.
+
+
+**No roles are deployed into your projects.** Unlike AWS Organizations onboarding, GCP onboarding deploys nothing in Google Cloud. Prowler reuses the organization credential you provide as the credential of every project it onboards, so a single grant covers discovery and scanning.
+
+
+## Before You Start
+
+### Grant Read Access at the Organization Node
+
+Discovery reads three Cloud Resource Manager resources: the organization itself, the folders beneath it, and the projects in each folder. Grant these permissions to the credential **directly on the organization**, not on a project:
+
+| Permission | Used for |
+|------------|----------|
+| `resourcemanager.organizations.get` | Reading the organization and its display name. |
+| `resourcemanager.folders.list` | Walking the folder hierarchy. |
+| `resourcemanager.projects.list` | Listing the projects in the organization and in every folder. |
+
+The **Browser (`roles/browser`)** predefined role covers all three. Scanning each project additionally needs the permissions described in [GCP Authentication in Prowler](/user-guide/providers/gcp/authentication#required-permissions) — **Viewer (`roles/viewer`)**, **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)**, and the custom `ProwlerRole`. Binding those at the organization node too means every project you onboard is scannable without a per-project grant:
+
+```bash
+ORG_ID=123456789012
+MEMBER="serviceAccount:prowler@.iam.gserviceaccount.com"
+
+# Discovery: read the organization, its folders, and its projects
+gcloud organizations add-iam-policy-binding "$ORG_ID" \
+ --member="$MEMBER" --role="roles/browser"
+
+# Scanning: read resources in every project under the organization
+gcloud organizations add-iam-policy-binding "$ORG_ID" \
+ --member="$MEMBER" --role="roles/viewer"
+
+gcloud organizations add-iam-policy-binding "$ORG_ID" \
+ --member="$MEMBER" --role="roles/serviceusage.serviceUsageConsumer"
+```
+
+### Enable the Cloud Resource Manager API
+
+Enable the Cloud Resource Manager API in the project that owns the credential — the service account's host project, or the quota project for user credentials:
+
+```bash
+gcloud services enable cloudresourcemanager.googleapis.com \
+ --project
+```
+
+### Find Your Organization ID
+
+Prowler identifies your organization by its numeric Google Cloud organization ID:
+
+```bash
+gcloud organizations list
+```
+
+In the Google Cloud console, the ID sits in the **ID** column next to the organization on the [Manage Resources](https://console.cloud.google.com/cloud-resource-manager) page, above the folders and projects it holds:
+
+
+
+
+
+## Step 1: Start the Organization Wizard
+
+### Open the Wizard
+
+1. Navigate to **Providers** and click **Add Provider**.
+
+
+
+
+
+2. Select **Google Cloud** as the provider.
+
+
+
+
+
+3. Choose **Add Multiple Projects With GCP Organization**.
+
+
+
+
+
+
+In Prowler Local Server the organization option is marked **Cloud** and opens an upgrade panel instead of the wizard. Organization-level onboarding is a Prowler Cloud feature; the single-project method remains available.
+
+
+### Enter Organization Details
+
+- **Organization ID**: the numeric ID of your Google Cloud organization (for example, `123456789012`). Non-numeric values are rejected before submission.
+- **Name** (optional): a display name for the organization in Prowler. If left blank, Prowler uses the name stored in Google Cloud.
+
+
+
+
+
+Click **Next** to proceed to the authentication phase. Prowler matches the organization by ID, so submitting an organization that is already onboarded reuses it instead of creating a duplicate.
+
+## Step 2: Authenticate with Google Cloud
+
+The **Authentication Details** step collects the credential Prowler uses to read your hierarchy and, later, to scan each project. Choose one of two methods.
+
+
+
+
+
+### Service Account Key
+
+Paste the full contents of a service account key file into **Service Account Key**. The field validates that the pasted text is a JSON object before submission.
+
+To create the key for the service account you granted access to:
+
+```bash
+gcloud iam service-accounts keys create prowler-key.json \
+ --iam-account=prowler@.iam.gserviceaccount.com
+```
+
+### Client ID, Client Secret and Refresh Token
+
+Use this method to authenticate as a Google account rather than a service account. It takes three values from an authorized-user credential:
+
+- **Client ID**
+- **Client Secret**
+- **Refresh Token**
+
+Running `gcloud auth application-default login` writes all three to `~/.config/gcloud/application_default_credentials.json`. The account must hold the roles listed in [Grant Read Access at the Organization Node](#grant-read-access-at-the-organization-node).
+
+
+Every project you onboard inherits this credential. Revoking it, rotating the key, or deleting the service account stops the scans of every project in the organization.
+
+
+### Authenticate and Discover
+
+Click **Authenticate**. Prowler then:
+
+- Creates the organization and stores the credential securely.
+- Triggers an asynchronous discovery that walks your hierarchy through the Cloud Resource Manager API.
+- Shows a **"Gathering GCP Projects..."** spinner while it waits.
+
+
+
+
+
+Discovery usually takes seconds to a couple of minutes, depending on how many folders and projects your organization holds.
+
+#### When Discovery Takes Too Long
+
+Prowler waits up to **3 minutes** for a result. Past that, the wizard stops waiting — but the discovery keeps running in Google Cloud — and offers two actions:
+
+- **Keep waiting** — resume the same discovery. Nothing is re-read from Google Cloud.
+- **Retry** — start a fresh discovery, which reads your hierarchy again.
+
+
+
+
+
+If discovery fails outright, the wizard explains why and offers **Retry discovery**. See [Troubleshooting](#troubleshooting) for each message.
+
+## Step 3: Select Projects to Scan
+
+### Understanding the Tree View
+
+Once discovery completes, the wizard renders your organization as a hierarchical tree:
+
+
+
+
+
+- **Folders** nest under the organization; projects created directly under the organization appear at the top level.
+- **Selecting a folder** selects every selectable project beneath it. A folder whose projects are only partly selected renders in an indeterminate state.
+- **Individual overrides**: deselect single projects even when the parent folder is selected.
+- The header tracks the selection as **"X of Y projects selected"**.
+- Only **ACTIVE** folders and projects appear. Projects pending deletion are not listed.
+- Folder hierarchies are read up to **10 levels** deep. Deeper organizations report an error at discovery — see [Troubleshooting](#troubleshooting).
+
+### Blocked Projects
+
+A project is shown grayed out and cannot be selected when onboarding it would conflict with something Prowler already stores. Hover the project to see the reason:
+
+| Reason | What it means |
+|--------|---------------|
+| `organization_conflict` | The project is already connected under a **different** Prowler organization. |
+| `organization_node_conflict` | The project is already grouped under a different folder in Prowler — for example, it moved in Google Cloud after it was onboarded. |
+| `provider_type_conflict` | A provider with the same identifier exists in Prowler for another cloud provider. |
+
+
+
+
+
+### Folders With Nothing to Select
+
+A folder that holds no projects, or whose projects are all blocked, is shown disabled with the note *"No projects available to select in this folder."* The folder still expands, so you can see the blocked projects it holds and why they are blocked.
+
+
+
+
+
+### Custom Aliases
+
+Each project row carries an editable name, prefilled with the project's display name. The alias is used only inside Prowler — it does not rename anything in Google Cloud. Folder names are read-only: Prowler stores the folder display name from Google Cloud.
+
+### Projects That Already Have Credentials
+
+Applying your selection stores the organization credential on every selected project. When a selected project is already connected to Prowler with its own credential, that credential is **overwritten** — so the wizard asks first, naming the affected projects:
+
+
+
+
+
+Click **Replace and continue** to proceed, or **Cancel** to adjust your selection.
+
+
+**Your existing data is safe.** A project already connected as an individual provider is **linked** to the organization, never duplicated: its historical scans and findings are preserved, and it does not count twice toward your subscription.
+
+
+## Step 4: Test Connections
+
+Click **Test Connections** to verify that Prowler can authenticate against each selected project. Prowler creates one provider per project — identified by its Google Cloud project ID — and then tests every connection.
+
+
+
+
+
+Each project shows a real-time status indicator:
+
+- **Spinner** — test in progress
+- **Green checkmark (✓)** — connection successful
+- **Red icon (✗)** — connection failed (hover to see the error)
+
+If every project connects successfully, you advance to the next step automatically.
+
+### When Some Tests Fail
+
+An error banner appears: **"There was a problem connecting to some projects. Hover each project to check the error."** You have two options:
+
+**a) Fix and retry:**
+
+1. Confirm the credential holds **Viewer** and **Service Usage Consumer** on the failing projects (or on the organization).
+2. Confirm the Identity and Access Management (IAM) API is enabled as described in [GCP Authentication in Prowler](/user-guide/providers/gcp/authentication#project-level-settings).
+3. Click **Test Connections** again — only the **failed projects are re-tested**. Projects that already passed are not tested again.
+
+**b) Skip and continue:**
+
+Click **Skip Connection Validation** to proceed with the projects that connected successfully. Failed projects stay onboarded and visible on the Providers page, but they are not scanned. This option appears only when at least one project connected.
+
+If **no project** connects, the banner instead reads *"No projects connected successfully. Fix the connection errors and retry before launching scans."* and you cannot proceed. Fix the underlying problem — see [Troubleshooting](#troubleshooting) — and retry.
+
+## Step 5: Launch Scans
+
+The Organizations wizard uses the same schedule controls described in [Scan Scheduling](/user-guide/tutorials/prowler-scan-scheduling#schedule-options).
+
+Click **Save**, **Save and launch scan**, or **Launch scan**, depending on the selected schedule option. A toast notification confirms whether the schedule was saved, scans were launched, or both, and links to the **Scans** page. Prowler then redirects to the **Providers** page. Scans launch only for projects that passed connection testing.
+
+
+
+
+
+After launching:
+
+- Scans appear on the **Scans** page as they start and complete.
+- Results populate the **Overview** and **Findings** pages.
+- On the **Providers** page, your projects are grouped under the organization and, when they live in a folder, under that folder.
+
+
+
+
+
+## Manage Your Organization After Onboarding
+
+Open the row actions menu on the organization row on the **Providers** page.
+
+
+
+
+
+| Action | What it does |
+|--------|--------------|
+| **Edit Organization Name** | Renames the organization in Prowler. Leave it blank to fall back to the name stored in Google Cloud. |
+| **Update Credentials** | Reopens the Authentication Details step to store a new credential. |
+| **Edit Scan Schedule** | Applies one schedule to every connected project in the organization. |
+| **Test Connections (N)** | Re-tests every project in the organization. |
+| **Delete Organization** | Deletes the organization and cascades to its providers. |
+
+### Onboard Projects Created Later
+
+Projects added to your Google Cloud organization after onboarding are not picked up automatically. Run the wizard again with the same organization ID: discovery returns the current hierarchy, already-connected projects come back preselected, and the new ones are ready to select.
+
+### Update Organization Credentials
+
+Choosing **Update Credentials** re-enters the Authentication Details step. Because the organization already holds a credential, Prowler warns before overwriting it and names how many providers re-authenticate with the new one:
+
+
+
+
+
+Storing a new credential runs a fresh discovery, so any discovery already in progress is discarded — discovery authenticates with the credential it started from.
+
+### Delete an Organization or Folder
+
+Deleting an organization or a folder **cascades to every provider grouped under it**, along with their scans and findings. Both dialogs state how many providers are affected before you confirm.
+
+
+
+
+
+Deletion runs in the background. Prowler confirms with a **"Deletion started"** notification; if any part of it fails, the affected rows reappear on a later refresh.
+
+
+Deleting an organization **permanently deletes every project provider grouped under it**, including their historical scans and findings. This action cannot be undone.
+
+
+### When Grouping Is Unavailable
+
+If Prowler cannot read your hierarchy while loading the Providers page, a notice reads *"Organization grouping is incomplete. Some providers may appear ungrouped."* Your providers are still listed, just flat. Reload the page to try again.
+
+## Billing Impact
+
+Each Google Cloud project you connect through the Organizations wizard counts as one **provider** in your Prowler Cloud subscription.
+
+- **Already-connected projects**: linking an existing provider to the organization does **not** add billing. The existing provider is reused.
+- **Large organizations**: connecting a 500-project organization results in up to 500 providers on your subscription. Review your plan limits before proceeding.
+- **Deleted providers**: a project you later remove no longer counts toward your subscription.
+
+For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
+
+## Troubleshooting
+
+### That Organization ID Is Not Valid
+
+*"That organization ID is not valid. Copy the numeric ID from the Google Cloud console and try again."*
+
+Google Cloud rejected the ID. Use only the digits — no `organizations/` prefix and no domain name. Run `gcloud organizations list` and copy the `ID` column.
+
+### No Organization With That ID Was Found
+
+*"No organization with that ID was found. Check the ID, and that the service account has been granted access to the organization."*
+
+Either the ID belongs to another organization, or the credential cannot see this one. Confirm the binding was created **on the organization** and not on a project:
+
+```bash
+gcloud organizations get-iam-policy \
+ --flatten="bindings[].members" \
+ --filter="bindings.members:" \
+ --format="table(bindings.role)"
+```
+
+### The Service Account Cannot List Folders and Projects
+
+*"The service account cannot list this organization's folders and projects. Grant it the Folder Viewer and Project Viewer roles at the organization level, then try again."*
+
+The credential authenticated but lacks read access to the hierarchy. Grant **Browser (`roles/browser`)** at the organization node, as described in [Grant Read Access at the Organization Node](#grant-read-access-at-the-organization-node), and confirm the Cloud Resource Manager API is enabled in the credential's project.
+
+### Authentication Failed
+
+*"Authentication failed. Please verify the service account permissions or credentials, then try again."*
+
+- For a service account key, confirm the key is still active and the pasted JSON is the full key file.
+- For client credentials, confirm the refresh token has not been revoked — `gcloud auth application-default login` issues a new one.
+- Confirm the service account itself is not disabled or deleted.
+
+### Google Cloud Did Not Respond
+
+*"Google Cloud did not respond while reading the organization. Nothing is wrong with your credentials — try again in a few minutes."*
+
+A transient Cloud Resource Manager error. Click **Retry discovery**.
+
+### The Folder Hierarchy Is Too Deep
+
+*"This organization's folder hierarchy is deeper than Prowler can read. Contact support so we can help you onboard it."*
+
+Prowler reads up to 10 levels of nested folders. Contact [Prowler Support](mailto:support@prowler.com).
+
+### Discovery Never Finishes
+
+The wizard stops waiting after 3 minutes, but the discovery keeps running in Google Cloud. Click **Keep waiting** to resume the same discovery rather than **Retry**, which starts over and re-reads your whole hierarchy.
+
+## Key Concepts
+
+### How Projects Map to Prowler Providers
+
+Each selected project becomes one Prowler provider:
+
+| Prowler field | Comes from |
+|---------------|------------|
+| Provider identifier | The Google Cloud project ID (for example, `prowler-prod-1`). |
+| Alias | The name you typed in the tree, or the project's display name. |
+| Credential | A copy of the organization credential. |
+
+Folders that hold selected projects become grouping rows on the Providers page. You select projects only — Prowler derives the folder ancestors itself.
+
+### Organization Credential vs Project Credential
+
+One credential, stored twice: on the organization, where discovery reads it, and on each project provider, where scans read it. That is why replacing the organization credential re-authenticates every project under it, and why the wizard asks before overwriting a project's own credential.
+
+## What's Next
+
+
+
+ Full guide to using Prowler Cloud features.
+
+
+ CLI-based scanning of a specific Google Cloud organization.
+
+
+ Credential types and the permissions Prowler needs in Google Cloud.
+
+
+ Script-based bulk provisioning for advanced automation.
+
+
diff --git a/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx b/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx
deleted file mode 100644
index 1ed5e8ff4a..0000000000
--- a/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx
+++ /dev/null
@@ -1,30 +0,0 @@
----
-title: 'Prowler Cloud Public Egress IPs'
-description: 'Query the dedicated Prowler Cloud egress IPv4 address to allowlist scans across AWS, Azure, GCP, Kubernetes clusters, and other customer network controls.'
----
-
-## Overview
-
-Prowler Cloud uses a dedicated egress IPv4 address for all outbound connections to customer infrastructure. This enables organizations to implement network-level security controls by whitelisting Prowler's IP address.
-
-## Use Cases
-
-Whitelisting Prowler's egress IP address enables:
-
-- **Credential Usage Control**: Restrict where cloud provider credentials can be used from across AWS, Azure, GCP, and other providers
-- **Kubernetes Security**: Limit inbound HTTPS traffic to clusters by allowing only Prowler's IP address
-- **Compliance Requirements**: Meet security policies requiring allowlisting of external services
-
-## Query the Egress IP Address
-
-Retrieve Prowler Cloud's current egress IP address using the following command:
-
-```bash
-dig egress.prowler.com +short
-```
-
-This command returns the IPv4 address that Prowler Cloud uses for all outbound connections to customer infrastructure.
-
-
-The egress IP address is stable, but it is recommended to periodically verify it remains current by querying `egress.prowler.com`.
-
diff --git a/docs/user-guide/tutorials/prowler-for-msps-billing.mdx b/docs/user-guide/tutorials/prowler-for-msps-billing.mdx
new file mode 100644
index 0000000000..cef7ccf6a2
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-billing.mdx
@@ -0,0 +1,62 @@
+---
+title: "Billing and Customer Plans in Prowler for MSPs and MSSPs"
+sidebarTitle: "Billing and Plans"
+---
+
+Each customer carries its own billing plan, set when the customer is created and changed later from the **Customers** page. This page covers both, and where the resulting revenue is reported.
+
+## Permissions
+
+Managing customer plans requires a role with **Manage billing**, held today by both **Superadmin** and **Organization Admin**. See [Managing Your Team](/user-guide/tutorials/prowler-for-msps-team).
+
+## Customer Plans
+
+Plans are chosen in the **Set Their Billing Plan** step of the Add Customer wizard. A customer starts on a trial or on one of the paid plans, billed monthly or annually.
+
+Each plan card in the wizard shows its own price, included usage and overage rate. For current pricing, see [prowler.com/pricing](https://prowler.com/pricing).
+
+### Provider Accounts on the Annual Plan
+
+The annual plan is paid upfront for a fixed number of cloud provider accounts, between **1 and 20**. You set that count when you pick the plan. The monthly plan does not require an upfront provider account count.
+
+## Change a Customer's Plan
+
+Open the actions menu on a customer's row and choose **Change plan**.
+
+
+**Plan changes are one way: trial to paid.** The action is only offered while a customer is on trial or its trial has expired. Once a customer holds a paid subscription, **Change plan** no longer appears on the row, and the trial is never a valid target.
+
+
+The **Change Plan** dialog opens on **Choose your plan**, with the same **Monthly** and **Annual** toggle used when the customer was created.
+
+
+
+Select a plan and confirm with **Change Plan**. Choosing the annual plan also asks for the upfront provider account count. The change is submitted to Prowler Cloud and applied asynchronously; the customer's row updates once it lands. If it fails, use the reported problem to identify the cause:
+
+| Problem | Cause |
+|---|---|
+| Cloud accounts count is required | The annual plan was selected without a provider account count. |
+| Cloud accounts count out of range | The count is outside 1–20. |
+| Company name is required | The customer record has no usable company name. |
+| Customer not found | The customer no longer exists or is not linked to a tenant. |
+
+## Revenue Reporting
+
+Billing figures surface in two places.
+
+**On the Customers page**, stat cards above the table summarize **Billing active** — how many customers hold an active subscription — and **Total MTD** per currency, with a percentage change against last month. The cards appear once a customer has billing activity.
+
+
+
+**On the Dashboard**, the **Billing Overview** card reports monthly expenses against the previous month and splits revenue for the period into annual, monthly and overage, giving the same picture across every customer.
+
+## Next Steps
+
+
+
+ Add customers and open their Prowler Cloud tenants.
+
+
+ Lifecycle, settings, Partner Code and closing your organization.
+
+
diff --git a/docs/user-guide/tutorials/prowler-for-msps-branding.mdx b/docs/user-guide/tutorials/prowler-for-msps-branding.mdx
new file mode 100644
index 0000000000..82ae8d1e08
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-branding.mdx
@@ -0,0 +1,58 @@
+---
+title: "Customizing Your Branding in Prowler for MSPs and MSSPs"
+sidebarTitle: "Branding"
+---
+
+Upload and save your company logo, then preview its intended placement alongside Prowler branding. Branding is managed from **Settings → Branding** and requires a role with **Manage settings**.
+
+## Upload a Logo
+
+Open **Settings → Branding**, click **Upload Logo**, and pick your file. The logo replaces the placeholder in Settings immediately and a confirmation appears.
+
+
+
+### Logo Requirements
+
+| Requirement | Value |
+|---|---|
+| **Formats** | PNG or SVG |
+| **Maximum file size** | 512 KB |
+| **Dimensions** | 200 × 60 pixels — a hard limit for PNG, not checked for SVG |
+
+
+Two limits on this screen are looser than what the server accepts:
+
+* The upload dialog accepts files up to 1 MB, but anything above **512 KB** is rejected.
+* The page describes 200 × 60 pixels as a recommended size. For PNG it is a **maximum**: a larger PNG is rejected with *"PNG dimensions must not exceed 200×60 px."* SVG is exempt from the dimension check.
+
+Keep PNG logos within both limits to avoid an upload that appears to start and then fails.
+
+
+A wide, horizontal logo with a transparent background renders best. SVG stays crisp at every size, is not subject to the dimension limit, and is the better choice where you have it.
+
+
+Uploaded SVG files are sanitized on the server. Scripts, external references and other active content are stripped before the file is stored.
+
+
+## Replace or Remove a Logo
+
+Uploading a new file replaces the saved logo. **Remove Logo** deletes it and returns the Settings preview to the *Your Logo* placeholder.
+
+## Logo Placement Preview
+
+Below the upload controls, **Logo Placement Preview** renders your logo underneath the Prowler wordmark to show the intended placement. With no logo uploaded, the slot shows a *Your Logo* placeholder.
+
+
+The saved logo is currently displayed only in the upload area and placement preview on this Settings page. It is not applied elsewhere in Partner Portal, Prowler Cloud or customer reports.
+
+
+## Next Steps
+
+
+
+ Lifecycle, settings, Partner Code and closing your organization.
+
+
+ Invite team members and assign roles.
+
+
diff --git a/docs/user-guide/tutorials/prowler-for-msps-customers.mdx b/docs/user-guide/tutorials/prowler-for-msps-customers.mdx
new file mode 100644
index 0000000000..b0433f045b
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-customers.mdx
@@ -0,0 +1,98 @@
+---
+title: "Onboarding Customers and Accessing Their Tenants"
+sidebarTitle: "Onboarding Customers"
+---
+
+Adding a customer in Prowler for MSPs and MSSPs provisions a Prowler Cloud tenant for that organization and links it to yours. From then on you can open that tenant from the console and work inside it on the customer's behalf.
+
+## Add a Customer
+
+If you are a Superadmin, select **Customers** in the sidebar, then click **Add Customer** to open a three-step wizard.
+
+
+In the Partner Portal UI, the **Add Customer** action is currently shown to Superadmins. Organization Admins manage existing customers but do not see the action.
+
+
+
+
+ Enter the **Customer Business Name** and confirm the **Region**. The name must be unique within your partner organization; a duplicate is rejected inline. Click **Next**.
+
+ 
+
+
+
+ Under **Choose your plan**, switch between **Monthly** and **Annual** and pick the plan the customer starts on. Click **Create Customer**. See [Billing and Customer Plans](/user-guide/tutorials/prowler-for-msps-billing) for what the annual plan asks for.
+
+ 
+
+
+
+ Wait while the tenant is created and linked to your organization. A banner then confirms that the customer was created on trial or that paid-plan enrollment was submitted. Click **Go To Organization** to head straight there and start connecting cloud providers, or **Close** to return to the customer list.
+
+ 
+
+
+
+Customer creation waits for tenant provisioning to finish. A successful submission adds the customer as **Active**. If you selected a paid plan, enrollment continues asynchronously and the billing status updates when it completes.
+
+## The Customers View
+
+**My Customers** lists every customer you can reach.
+
+
+
+Each row carries:
+
+| Column | What it shows |
+|---|---|
+| **Customer Business Name** | The customer's name |
+| **Providers** | Icons for each cloud provider connected in their tenant |
+| **Cloud Accounts** | Number of provider accounts under scan |
+| **Resources** | Resources discovered by the latest scan |
+| **Failed Findings** | Failed findings from the latest scan |
+| **Billing Type** | The customer's current plan, shown as **Trial**, **Pro Monthly** or **Pro Annual** |
+| **MTD** | Month-to-date spend |
+| **Last Month Expenses** | Previous month's total |
+| **Status** | The customer's current status |
+| **Last scan completed** | When the most recent scan finished |
+
+Above the table, search by name and filter by provider or status. The download button at the top right of the table exports the list.
+
+## Open a Customer's Prowler Cloud Tenant
+
+Open the actions menu at the end of a customer's row and choose **Access Organization**. You are redirected into that customer's tenant in Prowler Cloud, signed in as yourself acting on their behalf.
+
+While you are in the tenant you see what a customer administrator sees, and every action is recorded in the Prowler Cloud audit log against both your identity and the customer you are acting for.
+
+Opening a tenant requires a role with **Access tenants**. The action fails with a clear message if you lack permission, if the customer no longer exists, or if the tenant is not ready.
+
+
+
+**Change plan** only appears while the customer is on trial or its trial has expired. See [Billing and Customer Plans](/user-guide/tutorials/prowler-for-msps-billing).
+
+## Edit a Customer
+
+Choose **Edit** from the row actions to open the **Edit customer** panel and rename the customer. The new name must still be unique within the partner organization.
+
+## Link an Existing Customer with Your Partner Code
+
+Each approved partner organization carries a **Partner Code**, shown on **Settings → Profile** with the helper text *"Share this code with customers to link their accounts."* A customer who already runs Prowler Cloud can use that code to request a link to you, rather than having you provision a fresh tenant.
+
+
+The customer-side flow that consumes the Partner Code is rolling out progressively in Prowler Cloud. Confirm availability with your Prowler contact before sharing the code.
+
+
+## Customer Self-Access
+
+Your customers keep signing in to [cloud.prowler.com](https://cloud.prowler.com) with their own users. Your access is additive — it neither replaces nor restricts theirs.
+
+## Next Steps
+
+
+
+ Customer plans and revenue reporting.
+
+
+ Invite team members and assign roles.
+
+
diff --git a/docs/user-guide/tutorials/prowler-for-msps-organization.mdx b/docs/user-guide/tutorials/prowler-for-msps-organization.mdx
new file mode 100644
index 0000000000..e8763100b7
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-organization.mdx
@@ -0,0 +1,90 @@
+---
+title: "Managing Your Partner Organization"
+sidebarTitle: "Your Partner Organization"
+---
+
+Your partner organization is the top-level container in Prowler for MSPs and MSSPs. It holds your team, your branding, your Partner Code and every customer whose Prowler Cloud tenant you operate.
+
+## Lifecycle
+
+A partner organization moves through four states:
+
+| State | Meaning |
+|---|---|
+| **Pending email verification** | The first administrator has signed up but has not yet clicked the verification link. |
+| **Pending approval** | Email verified. Prowler is reviewing the application. |
+| **Active** | Approved. The organization can sign in, invite team members and onboard customers. |
+| **Rejected** | Prowler reviewed and declined the application. The account cannot sign in. |
+
+A rejection email carries the reason, categorized as **Incomplete documentation**, **Not eligible**, **Duplicate** or **Other**.
+
+## Settings
+
+Open **Settings** from the sidebar. The tabs depend on your role. Users with **Manage settings** see **Profile**, **Branding**, **Security** and a disabled **Notifications** tab. Users without **Manage settings** see only **Security**.
+
+Every signed-in user can change their own password. Editing the organization itself requires a role with **Manage settings**.
+
+### Profile
+
+The **Profile** tab shows the **Partner Information** card: your organization name, its current status, the date it joined, the Partner Code and an editable **Company Name**.
+
+
+
+* **Partner Code** — a read-only, Prowler-issued identifier in the form `PRW-00000`. The helper text reads *"Share this code with customers to link their accounts."* Copy it with the button at the end of the row.
+* **Company Name** — the display name used in the console, in invitations and in outbound email. Edit it and click **Save Changes**.
+
+### Branding
+
+Upload your logo. See [Customizing Your Branding](/user-guide/tutorials/prowler-for-msps-branding).
+
+### Security
+
+Change your own password. Users with **Manage settings** can also see the Danger zone described below, but only the partner owner can submit a deletion request. An Organization Admin sees the password form only.
+
+## Customer Capacity
+
+Each partner organization has a cap on how many customers it can hold at once. The default is **50**. To raise it, contact Prowler.
+
+## Closing Your Organization
+
+Deleting a partner organization is a request, not an immediate action.
+
+
+
+ Go to **Settings → Security**. The Danger zone requires **Manage settings**, and submitting its **Delete Partner** request is restricted to the partner owner.
+
+
+
+ Enter a required **Reason for deletion**. The UI accepts 10–1000 characters, and the API rejects reasons shorter than 10 characters. Then type `DELETE` in the confirmation field to enable the button and submit.
+
+
+
+ Filing the request notifies the Prowler team and sends a confirmation to the requester. The Danger zone then reports that a deletion request is already pending review. You and your team keep full access while it is pending.
+
+
+
+ The Prowler team coordinates the offboarding from there, including what happens to each customer tenant and when your organization is closed. Closing removes the partner organization, its team memberships and its branding assets, and invalidates every session.
+
+
+
+
+Closing a partner organization does not delete customer data in Prowler Cloud on its own. The Prowler team confirms the handling of each customer tenant as part of the offboarding.
+
+
+## Ownership
+
+One user is the **owner** of the partner organization — by default, whoever signed up. Ownership transfer is Prowler-assisted rather than self-service: contact Prowler to request it. Prowler staff can transfer ownership only when the partner has no customer organizations and the target is an active Superadmin. The previous owner keeps the Superadmin role, and the current owner cannot be removed from the team while they hold ownership.
+
+## Next Steps
+
+
+
+ Invite team members and assign roles.
+
+
+ Upload your logo.
+
+
+ Add customers and open their Prowler Cloud tenants.
+
+
diff --git a/docs/user-guide/tutorials/prowler-for-msps-sign-up.mdx b/docs/user-guide/tutorials/prowler-for-msps-sign-up.mdx
new file mode 100644
index 0000000000..068abf39aa
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-sign-up.mdx
@@ -0,0 +1,87 @@
+---
+title: "Sign Up and Sign In to Prowler for MSPs and MSSPs"
+sidebarTitle: "Sign Up and Sign In"
+---
+
+Sign-up for Prowler for MSPs and MSSPs is self-service, but activation requires approval from the Prowler team. The first administrator registers the partner organization; every other team member joins by invitation.
+
+## Sign Up
+
+
+
+ Go to [partners.prowler.com/sign-up](https://partners.prowler.com/sign-up), fill in **Full name**, **Company name**, **Email**, **Password** and **Confirm password**, then click **Create account**.
+
+ 
+
+
+
+ Prowler sends a verification email containing a one-time link valid for **24 hours**. Click it to confirm the address; your partner organization then moves to **Pending approval**.
+
+ If the link expires, request a new one at [partners.prowler.com/resend-verification](https://partners.prowler.com/resend-verification). Issuing a fresh link invalidates any earlier unused link for the same account.
+
+
+
+ Prowler reviews every new application. You receive an email when the organization is approved — its status becomes **Active** — or when it is rejected, along with the reason.
+
+
+
+ Once approved, sign in at [partners.prowler.com](https://partners.prowler.com) with the email and password you chose. You land on the Dashboard.
+
+
+
+## Password Requirements
+
+Every password in the console — at sign-up, when accepting an invitation, and on reset — must satisfy all of the following:
+
+| Requirement | Rule |
+|---|---|
+| **Length** | At least 12 characters |
+| **Uppercase** | At least 1 uppercase letter |
+| **Lowercase** | At least 1 lowercase letter |
+| **Number** | At least 1 digit |
+| **Special character** | At least 1 special character |
+| **Not common** | Rejected if it appears on the common-password list |
+
+## Sign In
+
+Sign in at [partners.prowler.com](https://partners.prowler.com) with your email and password, then click **Login**.
+
+
+
+Sign-in fails while the partner organization is not yet active:
+
+| Message | What it means |
+|---|---|
+| Invalid email or password | The credentials do not match an account. |
+| Please verify your email before signing in | Email verification is still outstanding. Open the verification email or request a fresh link. |
+| Your partner application is still under review | Prowler has not approved the application yet. |
+| Your partner application was not approved | The application was rejected. The account cannot sign in. |
+
+## Reset a Forgotten Password
+
+Click **Forgot Password?** on the sign-in screen and enter your email. Prowler sends a reset link valid for **15 minutes**. The reset page asks for a new password and a confirmation; on success you return to sign-in.
+
+
+The confirmation banner appears whether or not the email matches an account, so the screen never reveals which addresses are registered. Requesting a new link invalidates any earlier unused one.
+
+
+## Sessions
+
+Portal sessions currently use a sliding **23-hour** lifetime. Continued use may renew the session window, but you should expect to sign in again after extended inactivity. Selecting **Remember me** does not provide a seven-day Portal session.
+
+If session refresh or validation fails, you are redirected to the sign-in screen. Sign in again to continue.
+
+## Sign Out
+
+Open the user avatar in the top-right corner of any page and select **Sign out**. The session is cleared and you return to the sign-in form.
+
+## Next Steps
+
+
+
+ Invite team members and assign roles.
+
+
+ Lifecycle, settings, Partner Code and closing your organization.
+
+
diff --git a/docs/user-guide/tutorials/prowler-for-msps-team.mdx b/docs/user-guide/tutorials/prowler-for-msps-team.mdx
new file mode 100644
index 0000000000..a9dd79cdc3
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-for-msps-team.mdx
@@ -0,0 +1,85 @@
+---
+title: "Managing Your Team in Prowler for MSPs and MSSPs"
+sidebarTitle: "Managing Your Team"
+---
+
+Each partner organization has its own team and its own role catalog. Administrators invite team members by email and assign each one a role that governs what they can do.
+
+## Roles
+
+Two roles ship with every partner organization:
+
+* **Superadmin** — full account management. Invites members, adds and manages customers, edits branding and settings, and opens customer tenants.
+* **Organization Admin** — manages customers and billing, and opens customer tenants. Cannot invite members, change organization settings, or add new customers.
+
+Each role is a set of permission flags:
+
+| Permission | What it allows | Superadmin | Organization Admin |
+|---|---|:---:|:---:|
+| **Manage members** | Invite, re-invite, disable, enable and remove team members | ✓ | |
+| **Manage settings** | Edit the organization profile and branding | ✓ | |
+| **Manage billing** | Manage customer plans | ✓ | ✓ |
+| **Manage organizations** | Edit existing customer details | ✓ | ✓ |
+| **Access tenants** | Open a customer's Prowler Cloud tenant | ✓ | ✓ |
+
+
+The Prowler Cloud-side permission level for a team member is provisioned automatically as **Manager** and is managed in Prowler Cloud, not here. There is no Cloud role to pick at invitation time.
+
+
+## Invite a Team Member
+
+
+
+ Select **Team** in the sidebar. The entry only appears for roles with **Manage members**.
+
+
+
+ Click **Invite User**, enter the **Email**, pick a **User Role**, then click **Send Invite**. Each role option in the selector carries a one-line description of what it grants.
+
+ 
+
+
+
+ The team table lists **Name**, **Email**, **Role**, **Status** and **User Event** for members and pending invitations. A pending invitation may display as **Expired** after its expiry passes. Accepted invitations become member rows, while revoked invitations are no longer shown.
+
+
+
+The invitee receives an email with a one-time link, valid for **7 days**, that opens a public acceptance page. There they set their full name and a password, accept, and are sent to the sign-in screen.
+
+An email address can hold only one pending invitation at a time.
+
+## Re-Invite or Revoke
+
+For a **Pending** or **Expired** invitation, **Re-invite** sends a fresh link and resets the expiry. **Revoke** invalidates the invitation immediately — the recipient can no longer accept it.
+
+To re-issue an invitation that is still pending, use **Re-invite** rather than sending a second one.
+
+## Disable, Enable or Remove a Member
+
+Active members carry a **Disable** action. Disabling revokes access immediately but keeps the row in the table, flagged as disabled, so the audit trail survives.
+
+A disabled member can be:
+
+* **Enabled** — access is restored as it was.
+* **Re-invited** — a fresh invitation brings them back as a new active member. The invite dialog opens pre-filled with their address and its title changes to **Re-invite user**.
+
+
+The organization owner cannot be removed from the team while they hold ownership. Transfer ownership first.
+
+
+## Notes
+
+* The first administrator is created during sign-up and becomes the owner.
+* An email address can hold only one active membership in a given partner organization.
+* Permissions are scoped to one partner organization. A session for one organization carries no permissions in another.
+
+## Next Steps
+
+
+
+ Add customers and open their Prowler Cloud tenants.
+
+
+ Customer plans and revenue reporting.
+
+
diff --git a/docs/user-guide/tutorials/prowler-import-findings.mdx b/docs/user-guide/tutorials/prowler-import-findings.mdx
index 5b68e79919..c745847879 100644
--- a/docs/user-guide/tutorials/prowler-import-findings.mdx
+++ b/docs/user-guide/tutorials/prowler-import-findings.mdx
@@ -133,7 +133,7 @@ Only **Detection Finding** (`class_uid: 2004`) records are accepted. Other OCSF
The **Manage Ingestions** RBAC permission controls access to the ingestion endpoints. Without this permission, findings cannot be submitted via the API or `--push-to-cloud`.
-For more information about RBAC permissions, refer to the [Prowler App RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
+For more information about RBAC permissions, refer to the [Prowler Cloud RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
## Using the CLI
diff --git a/docs/user-guide/tutorials/prowler-scan-scheduling.mdx b/docs/user-guide/tutorials/prowler-scan-scheduling.mdx
index e50ec86cc6..811368d590 100644
--- a/docs/user-guide/tutorials/prowler-scan-scheduling.mdx
+++ b/docs/user-guide/tutorials/prowler-scan-scheduling.mdx
@@ -21,11 +21,11 @@ Before creating or editing scan schedules, ensure that:
## Schedule Options
-A Prowler Cloud or Enterprise subscription supports the following custom recurring schedule options. Prowler self-hosted runs a daily scan automatically and does not expose custom cadence controls.
+A Prowler Cloud or Prowler Private Cloud subscription supports the following custom recurring schedule options. Prowler Local Server runs a daily scan automatically and does not expose custom cadence controls.
-| Schedule Option | Description | Cloud & Enterprise | Self-Hosted |
-|-----------------|-------------|--------------------|-------------|
-| Daily | Runs one scan every day at the selected time. | Yes | Yes |
+| Schedule Option | Description | Prowler Cloud & Prowler Private Cloud | Prowler Local Server |
+|-----------------|-------------|---------------------------------------|----------------------|
+| Daily | Runs one scan every day at the selected time. | Yes | Automatic |
| Every 48 hours | Runs one scan every 48 hours, anchored to the selected time. | Yes | — |
| Weekly | Runs one scan every week on the selected day and time. | Yes | — |
| Monthly | Runs one scan every month on the selected day, from day 1 to day 28. | Yes | — |
@@ -84,7 +84,7 @@ To bulk edit provider schedules:
4. Click **Edit Scan Schedule (N)**, where **N** is the number of selected providers.
5. Save the schedule.
-For AWS Organizations and Organizational Unit rows, **Edit Scan Schedule** applies the schedule to the connected child providers in that group.
+For organization rows and their grouping rows — AWS organizational units, GCP folders — **Edit Scan Schedule** applies the schedule to the connected child providers in that group.
Bulk schedule edits apply one schedule to every selected provider. If the wrong providers are selected, Prowler applies the same cadence to unintended providers. To recover, reopen bulk edit with the correct selection or update affected provider schedules individually.
diff --git a/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx b/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx
index 04c00bee17..dc0abf7997 100644
--- a/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx
+++ b/docs/user-guide/tutorials/v2_to_v3_checks_mapping.mdx
@@ -17,7 +17,7 @@ checks_v4_v3_to_v2_mapping = {
"apigateway_restapi_public": "extra745",
"apigateway_restapi_logging_enabled": "extra722",
"apigateway_restapi_waf_acl_attached": "extra744",
- “apigatewayv2_api_access_logging_enabled": "extra7156",
+ "apigatewayv2_api_access_logging_enabled": "extra7156",
"apigatewayv2_api_authorizers_enabled": "extra7157",
"appstream_fleet_default_internet_access_disabled": "extra7193",
"appstream_fleet_maximum_session_duration": "extra7190",
diff --git a/mcp_server/.env.template b/mcp_server/.env.template
index 11b8caa724..10aabd84cf 100644
--- a/mcp_server/.env.template
+++ b/mcp_server/.env.template
@@ -1,3 +1,3 @@
-PROWLER_APP_API_KEY="pk_your_api_key_here"
+PROWLER_API_KEY="pk_your_api_key_here"
API_BASE_URL="https://api.prowler.com/api/v1"
PROWLER_MCP_TRANSPORT_MODE="stdio"
diff --git a/mcp_server/AGENTS.md b/mcp_server/AGENTS.md
index a82cc42e33..e786f9a5b7 100644
--- a/mcp_server/AGENTS.md
+++ b/mcp_server/AGENTS.md
@@ -15,6 +15,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Review changelog format and conventions | `prowler-changelog` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Working on MCP server tools | `prowler-mcp` |
+| Writing tests for the MCP server | `prowler-test-mcp` |
## Project Overview
@@ -25,7 +26,7 @@ The Prowler MCP Server provides AI agents access to the Prowler ecosystem throug
## CRITICAL RULES
### Tool Implementation
-- ALWAYS: Extend `BaseTool` ABC for Prowler App tools (auto-registration)
+- ALWAYS: Extend `BaseTool` ABC for Prowler tools (auto-registration)
- ALWAYS: Use `@mcp.tool()` decorator for Hub/Docs tools
- NEVER: Manually register BaseTool subclasses
- NEVER: Import tools directly in server.py
@@ -48,21 +49,21 @@ The Prowler MCP Server provides AI agents access to the Prowler ecosystem throug
### Three Sub-Servers
```python
-await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub")
-await prowler_mcp_server.import_server(app_mcp_server, prefix="prowler_app")
-await prowler_mcp_server.import_server(docs_mcp_server, prefix="prowler_docs")
+prowler_mcp_server.mount(hub_mcp_server, namespace="prowler_hub")
+prowler_mcp_server.mount(app_mcp_server, namespace="prowler")
+prowler_mcp_server.mount(docs_mcp_server, namespace="prowler_docs")
```
### Tool Naming
- `prowler_hub_*` - Catalog and compliance (no auth)
- `prowler_docs_*` - Documentation search (no auth)
-- `prowler_app_*` - Cloud/App management (auth required)
+- `prowler_*` - Prowler Cloud, Private Cloud & Local Server management (auth required)
---
## TECH STACK
-Python 3.12+ | FastMCP 2.13.1 | httpx (async) | Pydantic | uv
+Python 3.12+ | FastMCP 3.4.4 | httpx (async) | Pydantic | uv | pytest
---
@@ -85,9 +86,23 @@ mcp_server/prowler_mcp_server/
## COMMANDS
+From `mcp_server/`:
+
```bash
-cd mcp_server && uv run prowler-mcp # STDIO mode
-cd mcp_server && uv run prowler-mcp --transport http --port 8000 # HTTP mode
+cd mcp_server
+
+uv run prowler-mcp # STDIO mode
+uv run prowler-mcp --transport http --port 8000 # HTTP mode
+
+uv run pytest # Run the test suite
+uv run pytest tests/prowler_app/models # Run one area
+uv run pytest --cov=./prowler_mcp_server # With coverage
+```
+
+From the repository root:
+
+```bash
+make test-mcp # Run the MCP test suite exactly as CI does
```
---
@@ -100,3 +115,7 @@ cd mcp_server && uv run prowler-mcp --transport http --port 8000 # HTTP mode
- [ ] No hardcoded secrets
- [ ] Error handling returns structured responses
- [ ] Parameter descriptions use Pydantic `Field()`
+- [ ] Tests added under `mcp_server/tests/`, mirroring the source path below the
+ package root (`prowler_mcp_server/prowler_app/tools/` -> `tests/prowler_app/tools/`),
+ as the SDK does for `prowler/` -> `tests/`
+- [ ] `uv run pytest` passes
diff --git a/mcp_server/CHANGELOG.md b/mcp_server/CHANGELOG.md
index ec0a2e354f..c040aa17e7 100644
--- a/mcp_server/CHANGELOG.md
+++ b/mcp_server/CHANGELOG.md
@@ -4,6 +4,60 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
+## [0.10.0] (Prowler v5.38.0)
+
+### 🚀 Added
+
+- Test foundation for the MCP server with shared fixtures, JSON:API builders, mocked HTTP transports and CI coverage reporting [(#12291)](https://github.com/prowler-cloud/prowler/pull/12291)
+- Test coverage for the integrations tools and models, pinning the connection-check choreography and the Jira dispatch retry safety [(#12343)](https://github.com/prowler-cloud/prowler/pull/12343)
+- Container images now ship an SBOM and build provenance as OCI attestations [(#12352)](https://github.com/prowler-cloud/prowler/pull/12352)
+
+### 🔄 Changed
+
+- `prowler_send_findings_to_jira` now reports `safe_to_retry` on every outcome, true only when Prowler knows no Jira work item was created: a dispatch the API refused is retryable, one that failed on the server or got no answer is not [(#12343)](https://github.com/prowler-cloud/prowler/pull/12343)
+- `prowler_list_integrations` no longer requests the `configuration` it discards, now that the API tolerates a sparse fieldset without it [(#12343)](https://github.com/prowler-cloud/prowler/pull/12343)
+
+### 🔐 Security
+
+- Upgrade cryptography to 50.0.0, closing CVE-2026-69247 and CVE-2026-69249 [(#12356)](https://github.com/prowler-cloud/prowler/pull/12356)
+
+---
+
+## [0.9.1] (Prowler v5.37.1)
+
+### 🔐 Security
+
+- Bumped `fastmcp` and pinned `cryptography`, `joserfc`, `mcp` and `python-multipart`, clearing all 7 high-severity CVEs from the MCP image [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+
+---
+
+## [0.9.0] (Prowler v5.37.0)
+
+### 🚀 Added
+
+- Read-only user management tools `prowler_list_users`, `prowler_get_user`, and `prowler_get_current_user` for listing tenant users with their emails and identifying the authenticated user [(#12088)](https://github.com/prowler-cloud/prowler/pull/12088)
+- RBAC role tools `prowler_list_roles`, `prowler_get_role`, `prowler_get_user_roles`, and `prowler_set_user_role` for browsing roles and setting the role a user holds [(#12088)](https://github.com/prowler-cloud/prowler/pull/12088)
+- Integrations tools to manage Amazon S3, AWS Security Hub and Jira integrations, and to send findings to Jira [(#12138)](https://github.com/prowler-cloud/prowler/pull/12138)
+
+### 🔄 Changed
+
+- README now documents the Cloud-only `prowler_cloud_*` tools available on the hosted Prowler MCP (alerts, findings triage, scan scheduling, scan configurations), and corrects the Prowler Hub check count and the scan orchestration capabilities [(#12266)](https://github.com/prowler-cloud/prowler/pull/12266)
+
+### 🐞 Fixed
+
+- Memory leak in HTTP mode caused by streamable-HTTP sessions being retained for the process lifetime when clients never sent `DELETE /mcp`; the server now runs stateless [(#12235)](https://github.com/prowler-cloud/prowler/pull/12235)
+- `prowler_list_integrations` failing with a 500 error on tenants with a Jira integration, caused by the request leaving `configuration` out of the sparse fieldset [(#12259)](https://github.com/prowler-cloud/prowler/pull/12259)
+
+---
+
+## [0.8.0] (Prowler v5.35.0)
+
+### 🔄 Changed
+
+- Core Prowler tool namespace from the `prowler_app_*` prefix to `prowler_*` [(#12017)](https://github.com/prowler-cloud/prowler/pull/12017)
+
+---
+
## [0.7.2] (Prowler v5.28.1)
### 🐞 Fixed
diff --git a/mcp_server/README.md b/mcp_server/README.md
index e990f0f363..a4c5a736b0 100644
--- a/mcp_server/README.md
+++ b/mcp_server/README.md
@@ -6,21 +6,32 @@
## Key Capabilities
-### Prowler Cloud and Prowler App (Self-Managed)
+### Prowler Cloud, Prowler Private Cloud & Prowler Local Server
-Full access to Prowler Cloud platform and self-managed Prowler App for:
+Full access to your Prowler data (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server) for:
- **Findings Analysis**: Query, filter, and analyze security findings across all your cloud environments
- **Finding Groups Analysis**: Triage findings grouped by check ID and drill down into affected resources
- **Provider Management**: Create, configure, and manage your configured Prowler providers (AWS, Azure, GCP, etc.)
-- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments
+- **Scan Orchestration**: Trigger on-demand scans, track their progress, and schedule a daily scan
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting rules to suppress non-critical findings
- **Compliance Reporting**: View compliance status across frameworks and drill into requirement-level details
+- **Attack Paths Analysis**: Analyze privilege escalation chains through graph-based analysis of cloud resource relationships
+- **Integrations Management**: Set up and troubleshoot where Prowler sends its results (Amazon S3, AWS Security Hub, Jira), and turn findings into Jira work items
+- **User & Role Management**: List the users in your tenant, identify the authenticated user, browse RBAC roles, and set the role a user holds
+
+### Prowler Cloud Management
+
+Prowler Cloud-only workflow and configuration features (`prowler_cloud_*` tools). These are available only on the [hosted Prowler MCP](#1-hosted-prowler-mcp-recommended), since they manage features that exist only in Prowler Cloud:
+- **Scan Configurations**: Read, create, update, and delete reusable scan configurations and attach them to providers (providers without one use the default)
+- **Findings Triage**: Read and set a finding's triage status and leave notes documenting the decision, without suppressing the finding
+- **Scan Scheduling**: Read and configure recurring scan schedules (daily, interval, weekly, monthly), one provider at a time or in bulk
+- **Alerts**: Read and manage alert rules and recipients, dry-run rule conditions before saving, and browse the fired-alert history
### Prowler Hub
Access to Prowler's comprehensive security knowledge base:
-- **Security Checks Catalog**: Browse and search **over 1000 security checks** across multiple Prowler providers
+- **Security Checks Catalog**: Browse and search **over 2,000 security checks** across multiple Prowler providers
- **Check Implementation**: View the Python code that powers each security check
- **Automated Fixers**: Access remediation scripts for common security issues
- **Compliance Frameworks**: Explore mappings to **over 70 compliance standards and frameworks**
@@ -49,7 +60,7 @@ For comprehensive guides and tutorials, see the official documentation:
Prowler MCP Server can be used in three ways:
-### 1. Prowler Cloud MCP Server (Recommended)
+### 1. Hosted Prowler MCP (Recommended)
**Use Prowler's managed MCP server at `https://mcp.prowler.com/mcp`**
@@ -126,7 +137,8 @@ For complete tool descriptions and parameters, see the [Tools Reference](https:/
### Tool Naming Convention
All tools follow a consistent naming pattern with prefixes:
-- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
+- `prowler_*` - Prowler Cloud, Prowler Private Cloud & Prowler Local Server management tools
+- `prowler_cloud_*` - Prowler Cloud-only management tools (hosted Prowler MCP only)
- `prowler_hub_*` - Prowler Hub catalog and compliance tools
- `prowler_docs_*` - Prowler documentation search and retrieval
@@ -134,7 +146,7 @@ All tools follow a consistent naming pattern with prefixes:
```text
prowler_mcp_server/
-├── server.py # Main orchestrator (imports sub-servers with prefixes)
+├── server.py # Main orchestrator (mounts sub-servers with namespaces)
├── main.py # CLI entry point
├── prowler_hub/ # tools - no authentication required
├── prowler_app/ # tools - authentication required
@@ -146,7 +158,7 @@ prowler_mcp_server/
**Key Features:**
- **Modular Design**: Three independent sub-servers with prefixed namespacing
-- **Auto-Discovery**: Prowler App tools are automatically discovered and registered
+- **Auto-Discovery**: Prowler tools are automatically discovered and registered
- **LLM Optimization**: Response models minimize token usage by excluding empty values
- **Dual Transport**: Supports both STDIO (local) and HTTP (remote) modes
@@ -158,7 +170,15 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
- "Show me all critical findings from my AWS production accounts"
- "Register my new AWS account in Prowler and run a scheduled scan every day"
-- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
+- "List all muted findings and flag the ones whose mute reason is too weak for their severity"
+- "Send my failed CIS findings for this provider to Jira as work items"
+
+### Prowler Cloud Management
+
+- "Preview an alert rule for critical AWS findings and create it for my confirmed recipients"
+- "Show the triage notes for this finding and mark it as under review"
+- "Apply a weekly Monday 06:00 scan schedule to every AWS provider"
+- "Create a scan configuration that runs only CIS checks and attach it to my production providers"
### Security Research
@@ -174,17 +194,17 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
## Requirements
-**For Prowler Cloud MCP Server:**
-- Prowler Cloud account and API key (only for Prowler Cloud/App features)
+**For the hosted Prowler MCP:**
+- Prowler Cloud account and API key (only for Prowler features)
**For self-hosted STDIO/HTTP Mode:**
- Python 3.12+ or Docker
- Network access to:
- `https://hub.prowler.com` (for Prowler Hub)
- `https://docs.prowler.com` (for Prowler Documentation)
- - Prowler Cloud API or self-hosted Prowler App API (for Prowler Cloud/App features)
+ - Prowler Cloud API or Prowler Local Server API (for Prowler features)
-> **No Authentication Required**: Prowler Hub and Prowler Documentation features work without authentication. A Prowler API key is only required to access Prowler Cloud or Prowler App (Self-Managed) features.
+> **No Authentication Required**: Prowler Hub and Prowler Documentation features work without authentication. A Prowler API key is only required for the `prowler_*` and `prowler_cloud_*` tools (Prowler Cloud, Prowler Private Cloud, or Prowler Local Server).
## Configuring MCP Hosts
@@ -200,7 +220,7 @@ For developers looking to extend the MCP server with new tools or features:
## Related Products
- **[Prowler Hub](https://hub.prowler.com)**: Browse security checks and compliance frameworks
-- **[Prowler Cloud](https://cloud.prowler.com)**: Managed Prowler platform
+- **[Prowler Cloud](https://cloud.prowler.com)**: Fully managed Prowler in the cloud
- **[Lighthouse AI](https://docs.prowler.com/getting-started/products/prowler-lighthouse-ai)**: AI security analyst
## License
diff --git a/mcp_server/prowler_mcp_server/__init__.py b/mcp_server/prowler_mcp_server/__init__.py
index fe7af2dcea..aae427d274 100644
--- a/mcp_server/prowler_mcp_server/__init__.py
+++ b/mcp_server/prowler_mcp_server/__init__.py
@@ -5,7 +5,7 @@ This package provides MCP tools for accessing:
- Prowler Hub: All security artifacts (detections, remediations and frameworks) supported by Prowler
"""
-__version__ = "0.5.0"
+__version__ = "0.9.0"
__author__ = "Prowler Team"
__email__ = "engineering@prowler.com"
diff --git a/mcp_server/prowler_mcp_server/main.py b/mcp_server/prowler_mcp_server/main.py
index d502fddd52..411cff289a 100644
--- a/mcp_server/prowler_mcp_server/main.py
+++ b/mcp_server/prowler_mcp_server/main.py
@@ -48,6 +48,7 @@ def main():
host=args.host,
port=args.port,
show_banner=False,
+ stateless_http=True,
)
else:
logger.error(f"Invalid transport: {args.transport}")
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/__init__.py b/mcp_server/prowler_mcp_server/prowler_app/models/__init__.py
index 899ab4e866..1b15e35ac9 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/models/__init__.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/__init__.py
@@ -1,4 +1,4 @@
-"""Pydantic models for Prowler App MCP Server."""
+"""Pydantic models for Prowler MCP Server."""
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from prowler_mcp_server.prowler_app.models.findings import (
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py
index ae8431ba63..c2429012c3 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/finding_groups.py
@@ -228,7 +228,7 @@ class FindingGroupResource(MinimalSerializerMixin):
resource: FindingGroupResourceInfo = Field(description="Affected resource")
provider: FindingGroupProviderInfo = Field(description="Affected provider")
finding_id: str = Field(
- description="Finding UUID to use with prowler_app_get_finding_details"
+ description="Finding UUID to use with prowler_get_finding_details"
)
status: FindingStatus = Field(description="Finding status for this resource")
severity: FindingSeverity = Field(description="Finding severity")
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/integrations.py b/mcp_server/prowler_mcp_server/prowler_app/models/integrations.py
new file mode 100644
index 0000000000..ef7e3c318e
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/integrations.py
@@ -0,0 +1,330 @@
+"""Pydantic models for simplified integration responses."""
+
+from typing import Any, Literal
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
+
+
+class SimplifiedIntegration(MinimalSerializerMixin, BaseModel):
+ """Simplified integration for list operations.
+
+ Contains the identification and state fields needed to decide which integration
+ to inspect further, without the integration-type specific configuration.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(
+ description="Unique UUIDv4 identifier for this integration in Prowler database"
+ )
+ integration_type: str = Field(
+ description="Type of the integration. One of 'amazon_s3', 'aws_security_hub' or 'jira'"
+ )
+ enabled: bool = Field(
+ description="Whether this integration is active. Disabled integrations are never used after a scan and always fail the connection check"
+ )
+ connected: bool | None = Field(
+ default=None,
+ description="Result of the last connection check: True if the credentials work, False if they failed, null if the connection was never checked",
+ )
+ connection_last_checked_at: str | None = Field(
+ default=None,
+ description="ISO 8601 timestamp of the last connection check, null if it was never checked",
+ )
+ provider_ids: list[str] = Field(
+ default=[],
+ description="Prowler UUIDv4 identifiers of the providers this integration is attached to. Empty for tenant-wide integrations such as Jira",
+ )
+ inserted_at: str | None = Field(
+ default=None,
+ description="ISO 8601 timestamp when this integration was created",
+ )
+ updated_at: str | None = Field(
+ default=None,
+ description="ISO 8601 timestamp when this integration was last modified",
+ )
+
+ def _should_exclude(self, key: str, value: Any) -> bool:
+ """Override to always include the connected field even when None."""
+ # `null` means "never checked", which is different from "not connected"
+ if key == "connected":
+ return False
+ return super()._should_exclude(key, value)
+
+ @classmethod
+ def _extract_provider_ids(cls, data: dict[str, Any]) -> list[str]:
+ """Read the provider relationship linkage of a JSON:API integration resource."""
+ providers = data.get("relationships", {}).get("providers", {}).get("data") or []
+ return [provider["id"] for provider in providers]
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedIntegration":
+ """Transform JSON:API integration response to simplified format."""
+ attributes = data.get("attributes", {})
+
+ return cls(
+ id=data["id"],
+ integration_type=attributes["integration_type"],
+ enabled=attributes["enabled"],
+ connected=attributes.get("connected"),
+ connection_last_checked_at=attributes.get("connection_last_checked_at"),
+ provider_ids=cls._extract_provider_ids(data),
+ inserted_at=attributes.get("inserted_at"),
+ updated_at=attributes.get("updated_at"),
+ )
+
+
+class DetailedIntegration(SimplifiedIntegration):
+ """Detailed integration including its integration-type specific configuration.
+
+ Credentials are never returned by the Prowler API, so they are never part of this
+ model.
+ """
+
+ configuration: dict[str, Any] = Field(
+ default={},
+ description=(
+ "Integration-type specific settings. "
+ "For 'amazon_s3': 'bucket_name' and 'output_directory'. "
+ "For 'aws_security_hub': 'send_only_fails', 'archive_previous_findings' and "
+ "'enabled_regions' (the list of AWS regions Security Hub is enabled in, discovered by the connection check). "
+ "For 'jira': 'domain', 'projects' (a mapping of project key to project name) and "
+ "'issue_types' (a mapping of project key to the available issue types), all discovered by the connection check"
+ ),
+ )
+
+ @classmethod
+ def _build_configuration(
+ cls, integration_type: str, configuration: dict[str, Any]
+ ) -> dict[str, Any]:
+ """Normalize the raw configuration for LLM consumption."""
+ configuration = dict(configuration or {})
+
+ if integration_type == "aws_security_hub":
+ # The API stores every Security Hub region of the partition with a boolean,
+ # which is mostly noise. Only the enabled ones carry information.
+ regions = configuration.pop("regions", None)
+ if isinstance(regions, dict):
+ configuration["enabled_regions"] = sorted(
+ region for region, enabled in regions.items() if enabled
+ )
+ elif regions is not None:
+ # Unexpected shape, keep it as-is instead of dropping information
+ configuration["regions"] = regions
+
+ return configuration
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "DetailedIntegration":
+ """Transform JSON:API integration response to detailed format."""
+ attributes = data.get("attributes", {})
+ integration_type = attributes["integration_type"]
+
+ return cls(
+ id=data["id"],
+ integration_type=integration_type,
+ enabled=attributes["enabled"],
+ connected=attributes.get("connected"),
+ connection_last_checked_at=attributes.get("connection_last_checked_at"),
+ provider_ids=cls._extract_provider_ids(data),
+ inserted_at=attributes.get("inserted_at"),
+ updated_at=attributes.get("updated_at"),
+ configuration=cls._build_configuration(
+ integration_type, attributes.get("configuration", {})
+ ),
+ )
+
+
+class IntegrationsListResponse(BaseModel):
+ """Simplified response for integration list queries with pagination."""
+
+ model_config = ConfigDict(frozen=True)
+
+ integrations: list[SimplifiedIntegration] = Field(
+ description="List of simplified integrations matching the query filters"
+ )
+ total_num_integrations: int = Field(
+ description="Total number of integrations matching the query across all pages",
+ ge=0,
+ )
+ total_num_pages: int = Field(
+ description="Total number of pages available for the query results", ge=0
+ )
+ current_page: int = Field(
+ description="Current page number in the paginated results (1-indexed)", ge=1
+ )
+
+ @classmethod
+ def from_api_response(cls, response: dict[str, Any]) -> "IntegrationsListResponse":
+ """Transform JSON:API response to simplified format."""
+ data = response.get("data", [])
+ pagination = response.get("meta", {}).get("pagination", {})
+
+ return cls(
+ integrations=[
+ SimplifiedIntegration.from_api_response(item) for item in data
+ ],
+ total_num_integrations=pagination.get("count", 0),
+ total_num_pages=pagination.get("pages", 1),
+ current_page=pagination.get("page", 1),
+ )
+
+
+class IntegrationConnectionStatus(MinimalSerializerMixin, BaseModel):
+ """Result of an integration connection check."""
+
+ model_config = ConfigDict(frozen=True)
+
+ integration: DetailedIntegration = Field(
+ description="State of the integration after the connection check"
+ )
+ connected: Literal["connected", "failed", "not_tested"] = Field(
+ description="Outcome of the connection check: 'connected' if Prowler could reach the destination with the given credentials, 'failed' otherwise, 'not_tested' if the check did not run"
+ )
+ error: str | None = Field(
+ default=None,
+ description="Reason why the connection check failed, absent when it succeeded",
+ )
+
+ @classmethod
+ def create(
+ cls,
+ integration_data: dict[str, Any],
+ connection_status: dict[str, Any],
+ ) -> "IntegrationConnectionStatus":
+ """Create the connection status from the integration data and the check result.
+
+ Raises:
+ ValueError: If the check result carries an unexpected 'connected' value
+ """
+ match connection_status.get("connected"):
+ case True:
+ outcome = "connected"
+ case False:
+ outcome = "failed"
+ case None:
+ outcome = "not_tested"
+ case unexpected:
+ raise ValueError(
+ "Prowler returned an unexpected connection check result: 'connected' "
+ f"must be a boolean or null, got {unexpected!r}."
+ )
+
+ return cls(
+ integration=DetailedIntegration.from_api_response(integration_data),
+ connected=outcome,
+ error=connection_status.get("error", None),
+ )
+
+
+class JiraIssueTypes(MinimalSerializerMixin, BaseModel):
+ """Issue types available in a Jira project."""
+
+ model_config = ConfigDict(frozen=True)
+
+ project_key: str = Field(
+ description="Jira project key the issue types belong to (e.g. 'PROJ')"
+ )
+ issue_types: list[str] = Field(
+ description="Issue types that can be used when sending findings to this project (e.g. 'Task', 'Bug', 'Story')"
+ )
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "JiraIssueTypes":
+ """Transform JSON:API issue types response to simplified format.
+
+ Raises:
+ ValueError: If the payload does not carry the project key and its issue types
+ """
+ # This endpoint returns a non-model resource, so the unwrapped payload is accepted too
+ attributes = data.get("attributes")
+ if not isinstance(attributes, dict):
+ attributes = data
+
+ project_key = attributes.get("project_key")
+ issue_types = attributes.get("issue_types")
+
+ if not isinstance(project_key, str) or not isinstance(issue_types, list):
+ raise ValueError(
+ "Prowler returned an unexpected Jira issue types payload: expected a "
+ "'project_key' string and an 'issue_types' list, got the keys "
+ f"{sorted(attributes)}."
+ )
+
+ return cls(project_key=project_key, issue_types=issue_types)
+
+
+class JiraDispatchResult(MinimalSerializerMixin, BaseModel):
+ """Result of sending findings to Jira as work items."""
+
+ model_config = ConfigDict(frozen=True)
+
+ status: Literal["completed", "in_progress", "unknown", "failed"] = Field(
+ description="Outcome of the dispatch: 'completed' when Prowler finished creating the work items, 'in_progress' when the background task is still running, 'failed' when the dispatch was rejected before it started so nothing was created, 'unknown' when the dispatch stopped before reporting a result and Prowler cannot tell how many work items it had already created"
+ )
+ safe_to_retry: bool = Field(
+ description="True only when Prowler is certain that no Jira work item was created. When False the dispatch must NOT be sent again: some work items may already exist and retrying would duplicate them. Report the outcome to the user and let them check Jira instead"
+ )
+ created_count: int | None = Field(
+ default=None,
+ description="Number of Jira work items successfully created, absent unless the dispatch completed",
+ ge=0,
+ )
+ failed_count: int | None = Field(
+ default=None,
+ description="Number of findings that could not be sent to Jira, absent unless the dispatch completed",
+ ge=0,
+ )
+ error: str | None = Field(
+ default=None,
+ description="Reason why the dispatch failed or is still in progress, absent when it completed cleanly",
+ )
+ task_id: str | None = Field(
+ default=None,
+ description="UUIDv4 of the background task, present when the dispatch did not finish within the polling window so its state can be checked later",
+ )
+
+ def _should_exclude(self, key: str, value: Any) -> bool:
+ """Override to always include the known counters, even when zero."""
+ # A zero count is a meaningful outcome, not noise. An unknown one (None) is not
+ if key in ("created_count", "failed_count") and value is not None:
+ return False
+ return super()._should_exclude(key, value)
+
+ @classmethod
+ def from_task_result(
+ cls, result: Any, task_id: str | None = None
+ ) -> "JiraDispatchResult":
+ """Build the dispatch result from the completed background task result.
+
+ Raises:
+ ValueError: If the task result is not an object, or does not carry both
+ counters. Defaulting them to zero would report a dispatch as retryable
+ when it may have created work items
+ """
+ if not isinstance(result, dict):
+ raise ValueError(
+ "The completed dispatch task did not report a result object."
+ )
+
+ created_count = result.get("created_count")
+ failed_count = result.get("failed_count")
+
+ if not isinstance(created_count, int) or not isinstance(failed_count, int):
+ raise ValueError(
+ "The completed dispatch task did not report how many Jira work items it "
+ "created: expected 'created_count' and 'failed_count' integers, got the keys "
+ f"{sorted(result)}."
+ )
+
+ return cls(
+ status="completed",
+ # Work items are created one by one, so only an empty run can be repeated
+ safe_to_retry=created_count == 0,
+ created_count=created_count,
+ failed_count=failed_count,
+ error=result.get("error"),
+ task_id=task_id,
+ )
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/roles.py b/mcp_server/prowler_mcp_server/prowler_app/models/roles.py
new file mode 100644
index 0000000000..91499243c5
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/roles.py
@@ -0,0 +1,230 @@
+"""Data models for Prowler RBAC roles.
+
+This module provides Pydantic models for representing Prowler roles with
+two-tier complexity:
+- SimplifiedRole: For list operations with essential identification fields
+- DetailedRole: Extends simplified with the capabilities the role grants and
+ its related users / provider groups
+
+It also provides UserRolesResult, used by the tools that read or change the
+roles assigned to a specific user.
+
+All models inherit from MinimalSerializerMixin to exclude None/empty values
+for optimal LLM token usage.
+"""
+
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
+from prowler_mcp_server.prowler_app.models.utils import extract_relationship_ids
+
+# Role capabilities are exposed by the API as boolean "manage_*" attributes.
+_PERMISSION_ATTRIBUTE_PREFIX = "manage_"
+
+
+class SimplifiedRole(MinimalSerializerMixin, BaseModel):
+ """Simplified role representation for list operations.
+
+ Includes core identification fields for efficient overview.
+ Used by list_roles() tool.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(
+ description="Unique UUIDv4 identifier for this role in Prowler database"
+ )
+ name: str = Field(description="Human-readable name of the role")
+ permission_state: str | None = Field(
+ default=None,
+ description="Summary of the role's permissions: 'unlimited' (all), 'limited' (some), or 'none'",
+ )
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedRole":
+ """Transform a JSON:API role resource into a simplified model.
+
+ Args:
+ data: Role data from API response['data'] (single item or list item)
+
+ Returns:
+ SimplifiedRole instance
+ """
+ attributes = data["attributes"]
+
+ return cls(
+ id=data["id"],
+ name=attributes["name"],
+ permission_state=attributes.get("permission_state"),
+ )
+
+
+class DetailedRole(SimplifiedRole):
+ """Detailed role representation with granted capabilities and relationships.
+
+ Extends SimplifiedRole with the concrete management capabilities the role
+ grants, its visibility scope, and the IDs of related users and provider
+ groups. Used by get_role(), get_user_roles() and set_user_role().
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ permissions: list[str] | None = Field(
+ default=None,
+ description="Management capabilities granted by this role, as reported by the API (only the enabled ones), e.g. ['manage_users', 'manage_scans']. Deployments do not all expose the same capabilities, so read `permission_state` for the authoritative summary: 'unlimited' means the role grants every capability, including any not listed here.",
+ )
+ unlimited_visibility: bool | None = Field(
+ default=None,
+ description="Whether the role can see all providers (True) or only those in its provider groups (False)",
+ )
+ provider_group_ids: list[str] | None = Field(
+ default=None,
+ description="UUIDv4 identifiers of the provider groups this role is scoped to. An empty list means the role is not scoped to any provider group.",
+ )
+ user_ids: list[str] | None = Field(
+ default=None,
+ description="UUIDv4 identifiers of the users this role is assigned to. An empty list means the role is not assigned to any user.",
+ )
+ inserted_at: str | None = Field(
+ default=None, description="ISO 8601 timestamp when the role was created"
+ )
+ updated_at: str | None = Field(
+ default=None, description="ISO 8601 timestamp when the role was last modified"
+ )
+
+ def _should_exclude(self, key: str, value: Any) -> bool:
+ """Keep fields whose "empty" form carries meaning.
+
+ ``unlimited_visibility`` is kept even when ``False``, and ``permissions``
+ and the relationship lists are kept even when empty so that an empty
+ ``permissions``/``user_ids``/``provider_group_ids`` explicitly signals
+ "grants no capabilities / not assigned to any user / not scoped to any
+ provider group" instead of looking like an omitted, unknown field to an
+ agent.
+ """
+ if key in (
+ "unlimited_visibility",
+ "permissions",
+ "user_ids",
+ "provider_group_ids",
+ ):
+ return value is None
+ return super()._should_exclude(key, value)
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "DetailedRole":
+ """Transform a JSON:API role resource into a detailed model.
+
+ Args:
+ data: Role data from API response['data'] or an included role
+
+ Returns:
+ DetailedRole instance with all fields populated
+ """
+ attributes = data["attributes"]
+ relationships = data.get("relationships", {})
+
+ permissions = [
+ name
+ for name, enabled in attributes.items()
+ if name.startswith(_PERMISSION_ATTRIBUTE_PREFIX) and enabled
+ ]
+
+ return cls(
+ id=data["id"],
+ name=attributes["name"],
+ permission_state=attributes.get("permission_state"),
+ permissions=permissions,
+ unlimited_visibility=attributes.get("unlimited_visibility"),
+ provider_group_ids=extract_relationship_ids(
+ relationships, "provider_groups"
+ ),
+ user_ids=extract_relationship_ids(relationships, "users"),
+ inserted_at=attributes.get("inserted_at"),
+ updated_at=attributes.get("updated_at"),
+ )
+
+
+class RolesListResponse(BaseModel):
+ """Response model for list_roles() with pagination metadata.
+
+ Follows the established pattern from ScansListResponse and UsersListResponse.
+ """
+
+ roles: list[SimplifiedRole]
+ total_num_roles: int
+ total_num_pages: int
+ current_page: int
+
+ @classmethod
+ def from_api_response(cls, response: dict[str, Any]) -> "RolesListResponse":
+ """Transform a JSON:API list response into a roles list with pagination.
+
+ Args:
+ response: Full API response with data and meta
+
+ Returns:
+ RolesListResponse with simplified roles and pagination metadata
+ """
+ data = response.get("data", [])
+ meta = response.get("meta", {})
+ pagination = meta.get("pagination", {})
+
+ roles = [SimplifiedRole.from_api_response(item) for item in data]
+
+ return cls(
+ roles=roles,
+ total_num_roles=pagination.get("count", 0),
+ total_num_pages=pagination.get("pages", 0),
+ current_page=pagination.get("page", 1),
+ )
+
+
+class UserRolesResult(MinimalSerializerMixin, BaseModel):
+ """The roles currently assigned to a user.
+
+ Used by get_user_roles() to report a user's roles, and by set_user_role()
+ to report the authoritative role set after a change (with `changed` and
+ `message` describing the outcome).
+ """
+
+ user_id: str = Field(description="UUIDv4 identifier of the user")
+ total_num_roles: int = Field(
+ description="Number of roles currently assigned to the user"
+ )
+ roles: list[DetailedRole] = Field(
+ description="The roles currently assigned to the user, with their granted capabilities"
+ )
+ changed: bool | None = Field(
+ default=None,
+ description="For assignment operations: whether this call actually modified the user's roles",
+ )
+ message: str | None = Field(
+ default=None,
+ description="For assignment operations: human-readable description of the outcome",
+ )
+
+ def _should_exclude(self, key: str, value: Any) -> bool:
+ """Always include the roles list, even when empty (explicit 'no roles')."""
+ if key == "roles":
+ return False
+ return super()._should_exclude(key, value)
+
+ @classmethod
+ def build(
+ cls,
+ user_id: str,
+ roles: list[DetailedRole],
+ changed: bool | None = None,
+ message: str | None = None,
+ ) -> "UserRolesResult":
+ """Assemble a result from a user's role list, filling the count."""
+ return cls(
+ user_id=user_id,
+ total_num_roles=len(roles),
+ roles=roles,
+ changed=changed,
+ message=message,
+ )
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/users.py b/mcp_server/prowler_mcp_server/prowler_app/models/users.py
new file mode 100644
index 0000000000..0dcd4ac501
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/users.py
@@ -0,0 +1,143 @@
+"""Data models for Prowler users.
+
+This module provides Pydantic models for representing Prowler users with
+two-tier complexity:
+- SimplifiedUser: For list operations with essential identification fields
+- DetailedUser: Extends simplified with account metadata and role/membership links
+
+All models inherit from MinimalSerializerMixin to exclude None/empty values
+for optimal LLM token usage.
+"""
+
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
+from prowler_mcp_server.prowler_app.models.utils import extract_relationship_ids
+
+
+class SimplifiedUser(MinimalSerializerMixin, BaseModel):
+ """Simplified user representation for list operations.
+
+ Includes core identification fields for efficient overview.
+ Used by list_users() tool.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(
+ description="Unique UUIDv4 identifier for this user in Prowler database"
+ )
+ name: str = Field(description="Display name of the user")
+ email: str = Field(description="Email address of the user")
+ company_name: str | None = Field(
+ default=None, description="Company the user belongs to, if provided"
+ )
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedUser":
+ """Transform a JSON:API user resource into a simplified model.
+
+ Args:
+ data: User data from API response['data'] (single item or list item)
+
+ Returns:
+ SimplifiedUser instance
+ """
+ attributes = data["attributes"]
+
+ return cls(
+ id=data["id"],
+ name=attributes["name"],
+ email=attributes["email"],
+ company_name=attributes.get("company_name"),
+ )
+
+
+class DetailedUser(SimplifiedUser):
+ """Detailed user representation with account metadata and relationships.
+
+ Extends SimplifiedUser with the join date and the IDs of the roles and
+ memberships associated with the user.
+ Used by get_user() and get_current_user() tools.
+
+ Note: ``role_ids`` and ``membership_ids`` are omitted when empty because an
+ empty list is ambiguous here. The API hides another user's roles/memberships
+ from callers without MANAGE_ACCOUNT (returning them empty rather than
+ forbidden), so an empty list cannot be told apart from "genuinely none". They
+ are only reported when at least one ID is visible.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ date_joined: str | None = Field(
+ default=None,
+ description="ISO 8601 timestamp when the user joined",
+ )
+ role_ids: list[str] | None = Field(
+ default=None,
+ description="UUIDv4 identifiers of the roles assigned to the user (omitted when none are visible)",
+ )
+ membership_ids: list[str] | None = Field(
+ default=None,
+ description="UUIDv4 identifiers of the tenant memberships of the user (omitted when none are visible)",
+ )
+
+ @classmethod
+ def from_api_response(cls, data: dict[str, Any]) -> "DetailedUser":
+ """Transform a JSON:API user resource into a detailed model.
+
+ Args:
+ data: User data from API response['data']
+
+ Returns:
+ DetailedUser instance with all fields populated
+ """
+ attributes = data["attributes"]
+ relationships = data.get("relationships", {})
+
+ return cls(
+ id=data["id"],
+ name=attributes["name"],
+ email=attributes["email"],
+ company_name=attributes.get("company_name"),
+ date_joined=attributes.get("date_joined"),
+ role_ids=extract_relationship_ids(relationships, "roles"),
+ membership_ids=extract_relationship_ids(relationships, "memberships"),
+ )
+
+
+class UsersListResponse(BaseModel):
+ """Response model for list_users() with pagination metadata.
+
+ Follows the established pattern from ScansListResponse and ProvidersListResponse.
+ """
+
+ users: list[SimplifiedUser]
+ total_num_users: int
+ total_num_pages: int
+ current_page: int
+
+ @classmethod
+ def from_api_response(cls, response: dict[str, Any]) -> "UsersListResponse":
+ """Transform a JSON:API list response into a users list with pagination.
+
+ Args:
+ response: Full API response with data and meta
+
+ Returns:
+ UsersListResponse with simplified users and pagination metadata
+ """
+ data = response.get("data", [])
+ meta = response.get("meta", {})
+ pagination = meta.get("pagination", {})
+
+ users = [SimplifiedUser.from_api_response(item) for item in data]
+
+ return cls(
+ users=users,
+ total_num_users=pagination.get("count", 0),
+ total_num_pages=pagination.get("pages", 0),
+ current_page=pagination.get("page", 1),
+ )
diff --git a/mcp_server/prowler_mcp_server/prowler_app/models/utils.py b/mcp_server/prowler_mcp_server/prowler_app/models/utils.py
new file mode 100644
index 0000000000..a20687c03a
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/models/utils.py
@@ -0,0 +1,45 @@
+"""Shared helpers for building models from Prowler API responses.
+
+Stateless utilities used by the models' ``from_api_response()`` factory methods
+to read the JSON:API document structure (relationships, linkage, etc.). Keeping
+them here leaves ``base.py`` focused on the base model/mixin and gives these
+response-parsing helpers a single, discoverable home.
+"""
+
+from typing import Any
+
+
+def extract_relationship_ids(
+ relationships: dict[str, Any], relationship_name: str
+) -> list[str] | None:
+ """Extract related resource IDs from a JSON:API relationship.
+
+ Handles both to-one (``data`` is an object) and to-many (``data`` is a list)
+ relationships, returning a flat list of IDs in either case.
+
+ The absent and present-but-empty cases are deliberately distinguished so
+ callers can tell "the relationship was not part of this document" from "the
+ relationship is genuinely empty":
+
+ - Relationship key absent → ``None`` (unknown; the serializer did not expose
+ it, e.g. a role included via ``?include=roles`` carries no ``users``).
+ - Relationship key present but with no members → ``[]`` (explicitly none).
+
+ Args:
+ relationships: The ``relationships`` object from a JSON:API resource
+ relationship_name: The relationship key to read (e.g. ``"roles"``)
+
+ Returns:
+ List of related resource IDs, ``[]`` if the relationship is present but
+ empty, or ``None`` if the relationship is absent from the document.
+ """
+ relationship = relationships.get(relationship_name)
+ if relationship is None:
+ return None
+ data = relationship.get("data")
+ if not data:
+ return []
+ if isinstance(data, list):
+ return [item["id"] for item in data if item and item.get("id")]
+ # to-one relationship
+ return [data["id"]] if data.get("id") else []
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/__init__.py b/mcp_server/prowler_mcp_server/prowler_app/tools/__init__.py
index 4d740b6efe..8b5be76076 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/__init__.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/__init__.py
@@ -1,4 +1,4 @@
-"""Domain-specific tools for Prowler App MCP Server.
+"""Domain-specific tools for Prowler MCP Server.
Each module in this package contains a BaseTool subclass that registers
and implements tools for a specific domain (findings, providers, scans, etc.).
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py
index b08bbfe01f..5bd66760fa 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/attack_paths.py
@@ -1,4 +1,4 @@
-"""Attack Paths tools for Prowler App MCP Server.
+"""Attack Paths tools for Prowler MCP Server.
This module provides tools for analyzing Attack Paths data from Neo4j graph database.
Attack Paths help identify security risks by tracing potential attack vectors
@@ -22,16 +22,16 @@ class AttackPathsTools(BaseTool):
"""Tools for Attack Paths analysis.
Provides tools for:
- - prowler_app_list_attack_paths_scans: Find completed scans ready for analysis
- - prowler_app_list_attack_paths_queries: Discover available queries for a scan
- - prowler_app_run_attack_paths_query: Execute query and analyze attack paths
+ - prowler_list_attack_paths_scans: Find completed scans ready for analysis
+ - prowler_list_attack_paths_queries: Discover available queries for a scan
+ - prowler_run_attack_paths_query: Execute query and analyze attack paths
"""
async def list_attack_paths_scans(
self,
provider_id: list[str] = Field(
default=[],
- description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s). Use `prowler_app_search_providers` tool to find provider IDs",
+ description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s). Use `prowler_search_providers` tool to find provider IDs",
),
provider_type: list[str] = Field(
default=[],
@@ -73,8 +73,8 @@ class AttackPathsTools(BaseTool):
Workflow:
1. Use this tool to find completed attack paths scans
- 2. Use prowler_app_list_attack_paths_queries to see available queries for a scan
- 3. Use prowler_app_run_attack_paths_query to execute analysis
+ 2. Use prowler_list_attack_paths_queries to see available queries for a scan
+ 3. Use prowler_run_attack_paths_query to execute analysis
"""
try:
# Validate pagination
@@ -113,7 +113,7 @@ class AttackPathsTools(BaseTool):
async def list_attack_paths_queries(
self,
scan_id: str = Field(
- description="UUID of a COMPLETED attack paths scan. Use `prowler_app_list_attack_paths_scans` with state=['completed'] to find scan IDs"
+ description="UUID of a COMPLETED attack paths scan. Use `prowler_list_attack_paths_scans` with state=['completed'] to find scan IDs"
),
) -> list[dict[str, Any]]:
"""Discover available Attack Paths queries for a completed scan.
@@ -133,9 +133,9 @@ class AttackPathsTools(BaseTool):
- aws-ec2-instances-internet-exposed: Find internet-exposed EC2 instances
Workflow:
- 1. Use prowler_app_list_attack_paths_scans to find a completed scan
+ 1. Use prowler_list_attack_paths_scans to find a completed scan
2. Use this tool to discover available queries
- 3. Use prowler_app_run_attack_paths_query with query_id and any required parameters
+ 3. Use prowler_run_attack_paths_query with query_id and any required parameters
"""
try:
api_response = await self.api_client.get(
@@ -158,7 +158,7 @@ class AttackPathsTools(BaseTool):
description="UUID of a COMPLETED attack paths scan. The scan must be in 'completed' state"
),
query_id: str = Field(
- description="Query ID to execute (e.g., 'aws-internet-exposed-ec2-sensitive-s3-access'). Use `prowler_app_list_attack_paths_queries` to discover available queries"
+ description="Query ID to execute (e.g., 'aws-internet-exposed-ec2-sensitive-s3-access'). Use `prowler_list_attack_paths_queries` to discover available queries"
),
parameters: dict[str, str] = Field(
default_factory=dict,
@@ -194,7 +194,7 @@ class AttackPathsTools(BaseTool):
Workflow:
1. Ensure scan is completed
- 2. List available queries (use prowler_app_list_attack_paths_queries)
+ 2. List available queries (use prowler_list_attack_paths_queries)
3. Execute this tool with appropriate parameters
4. Analyze the returned graph for security insights
"""
@@ -231,7 +231,7 @@ class AttackPathsTools(BaseTool):
async def get_attack_paths_cartography_schema(
self,
scan_id: str = Field(
- description="UUID of a COMPLETED attack paths scan. Use `prowler_app_list_attack_paths_scans` with state=['completed'] to find scan IDs"
+ description="UUID of a COMPLETED attack paths scan. Use `prowler_list_attack_paths_scans` with state=['completed'] to find scan IDs"
),
) -> dict[str, Any]:
"""Retrieve the Cartography graph schema for a completed attack paths scan.
@@ -253,10 +253,10 @@ class AttackPathsTools(BaseTool):
- schema_content: Full Cartography schema markdown with node/relationship definitions
Workflow:
- 1. Use prowler_app_list_attack_paths_scans to find a completed scan
+ 1. Use prowler_list_attack_paths_scans to find a completed scan
2. Use this tool to get the schema for the scan's provider
3. Use the schema to craft custom openCypher queries
- 4. Execute queries with prowler_app_run_attack_paths_query
+ 4. Execute queries with prowler_run_attack_paths_query
"""
try:
api_response = await self.api_client.get(
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py b/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py
index 360dd5510d..33cdd22a69 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/compliance.py
@@ -1,4 +1,4 @@
-"""Compliance framework tools for Prowler App MCP Server.
+"""Compliance framework tools for Prowler MCP Server.
This module provides tools for viewing compliance status and requirement details
across all cloud providers.
@@ -50,7 +50,7 @@ class ComplianceTools(BaseTool):
if not scans_data:
raise ValueError(
f"No completed scans found for provider {provider_id}. "
- "Run a scan first using prowler_app_trigger_scan."
+ "Run a scan first using prowler_trigger_scan."
)
scan_id = scans_data[0]["id"]
@@ -60,11 +60,11 @@ class ComplianceTools(BaseTool):
self,
scan_id: str | None = Field(
default=None,
- description="UUID of a specific scan to get compliance data for. Required if provider_id is not specified. Use `prowler_app_list_scans` to find scan IDs.",
+ description="UUID of a specific scan to get compliance data for. Required if provider_id is not specified. Use `prowler_list_scans` to find scan IDs.",
),
provider_id: str | None = Field(
default=None,
- description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_app_search_providers` tool to find provider IDs.",
+ description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_search_providers` tool to find provider IDs.",
),
) -> dict[str, Any]:
"""Get high-level compliance overview across all frameworks for a specific scan.
@@ -90,11 +90,11 @@ class ComplianceTools(BaseTool):
Workflow:
1. Use this tool to get an overview of all compliance frameworks
- 2. Use prowler_app_get_compliance_framework_state_details with a specific compliance_id to see which requirements failed
+ 2. Use prowler_get_compliance_framework_state_details with a specific compliance_id to see which requirements failed
"""
if not scan_id and not provider_id:
return {
- "error": "Either scan_id or provider_id must be provided. Use prowler_app_search_providers to find provider IDs or prowler_app_list_scans to find scan IDs."
+ "error": "Either scan_id or provider_id must be provided. Use prowler_search_providers to find provider IDs or prowler_list_scans to find scan IDs."
}
elif scan_id and provider_id:
return {
@@ -254,7 +254,7 @@ class ComplianceTools(BaseTool):
async def get_compliance_framework_state_details(
self,
compliance_id: str = Field(
- description="Compliance framework ID to get details for (e.g., 'cis_1.5_aws', 'pci_dss_v4.0_aws'). You can get compliance IDs from prowler_app_get_compliance_overview or consulting Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
+ description="Compliance framework ID to get details for (e.g., 'cis_1.5_aws', 'pci_dss_v4.0_aws'). You can get compliance IDs from prowler_get_compliance_overview or consulting Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
),
scan_id: str | None = Field(
default=None,
@@ -262,14 +262,14 @@ class ComplianceTools(BaseTool):
),
provider_id: str | None = Field(
default=None,
- description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_app_search_providers` tool to find provider IDs.",
+ description="Prowler's internal UUID (v4) for a specific provider. If provided without scan_id, the tool will automatically find the latest completed scan for this provider. Use `prowler_search_providers` tool to find provider IDs.",
),
) -> dict[str, Any]:
"""Get detailed requirement-level breakdown for a specific compliance framework.
IMPORTANT: This tool returns DETAILED requirement information for a single compliance framework,
focusing on FAILED requirements and their associated FAILED finding IDs.
- Use this after prowler_app_get_compliance_overview to drill down into specific frameworks.
+ Use this after prowler_get_compliance_overview to drill down into specific frameworks.
The markdown report includes:
@@ -280,7 +280,7 @@ class ComplianceTools(BaseTool):
2. Failed Requirements Breakdown:
- Each failed requirement's ID and description
- Associated failed finding IDs for each failed requirement
- - Use prowler_app_get_finding_details with these finding IDs for more details and remediation guidance
+ - Use prowler_get_finding_details with these finding IDs for more details and remediation guidance
Default behavior:
- Requires either scan_id OR provider_id
@@ -289,14 +289,14 @@ class ComplianceTools(BaseTool):
- Only shows failed requirements with their associated failed finding IDs
Workflow:
- 1. Use prowler_app_get_compliance_overview to identify frameworks with failures
+ 1. Use prowler_get_compliance_overview to identify frameworks with failures
2. Use this tool with the compliance_id to see failed requirements and their finding IDs
- 3. Use prowler_app_get_finding_details with the finding IDs to get remediation guidance
+ 3. Use prowler_get_finding_details with the finding IDs to get remediation guidance
"""
# Validate that either scan_id or provider_id is provided
if not scan_id and not provider_id:
return {
- "error": "Either scan_id or provider_id must be provided. Use prowler_app_search_providers to find provider IDs or prowler_app_list_scans to find scan IDs."
+ "error": "Either scan_id or provider_id must be provided. Use prowler_search_providers to find provider IDs or prowler_list_scans to find scan IDs."
}
# Resolve provider_id to latest scan_id if needed
@@ -395,7 +395,7 @@ class ComplianceTools(BaseTool):
report_lines.append("**Failed Finding IDs**: None found")
report_lines.append("")
report_lines.append(
- "*Use `prowler_app_get_finding_details` with these finding IDs to get remediation guidance.*"
+ "*Use `prowler_get_finding_details` with these finding IDs to get remediation guidance.*"
)
report_lines.append("")
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py
index 905a352740..05adf8db2b 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/finding_groups.py
@@ -1,4 +1,4 @@
-"""Finding Groups tools for Prowler App MCP Server.
+"""Finding Groups tools for Prowler MCP Server.
This module provides read-only tools for finding group triage and drill-downs.
"""
@@ -233,8 +233,8 @@ class FindingGroupsTools(BaseTool):
`date_to`, this uses `/finding-groups` with a maximum 2-day date window.
Use this tool to find noisy or high-impact checks, then call
- prowler_app_get_finding_group_details for complete counters or
- prowler_app_list_finding_group_resources to drill into affected resources.
+ prowler_get_finding_group_details for complete counters or
+ prowler_list_finding_group_resources to drill into affected resources.
"""
try:
self.api_client.validate_page_size(page_size)
@@ -423,7 +423,7 @@ class FindingGroupsTools(BaseTool):
Default behavior returns FAIL, unmuted resources so the result is
actionable. Set `include_muted=True` to include accepted/suppressed
resources too. Each row includes nested resource and provider data plus
- `finding_id`. Use `prowler_app_get_finding_details(finding_id)` to
+ `finding_id`. Use `prowler_get_finding_details(finding_id)` to
retrieve complete remediation guidance for a specific resource finding.
"""
try:
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/findings.py b/mcp_server/prowler_mcp_server/prowler_app/tools/findings.py
index ec492c6a43..b556101cab 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/findings.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/findings.py
@@ -1,4 +1,4 @@
-"""Security Findings tools for Prowler App MCP Server.
+"""Security Findings tools for Prowler MCP Server.
This module provides tools for searching, viewing, and analyzing security findings
across all cloud providers.
@@ -92,7 +92,7 @@ class FindingsTools(BaseTool):
"""Search and filter security findings across all cloud providers with rich filtering capabilities.
IMPORTANT: This tool returns LIGHTWEIGHT findings. Use this for fast searching and filtering across many findings.
- For complete details use prowler_app_get_finding_details on specific findings.
+ For complete details use prowler_get_finding_details on specific findings.
Default behavior:
- Returns latest findings from most recent scans (no date parameters needed)
@@ -111,7 +111,7 @@ class FindingsTools(BaseTool):
Workflow:
1. Use this tool to search and filter findings by severity, status, provider, service, region, etc.
- 2. Use prowler_app_get_finding_details with the finding 'id' to get complete information about the finding
+ 2. Use prowler_get_finding_details with the finding 'id' to get complete information about the finding
"""
# Validate page_size parameter
self.api_client.validate_page_size(page_size)
@@ -187,9 +187,9 @@ class FindingsTools(BaseTool):
"""Retrieve comprehensive details about a specific security finding by its ID.
IMPORTANT: This tool returns COMPLETE finding details.
- Use this after finding a specific finding via prowler_app_search_security_findings
+ Use this after finding a specific finding via prowler_search_security_findings
- This tool provides ALL information that prowler_app_search_security_findings returns PLUS:
+ This tool provides ALL information that prowler_search_security_findings returns PLUS:
1. Check Metadata (information about the check script that generated the finding):
- title: Human-readable phrase used to summarize the check
@@ -217,7 +217,7 @@ class FindingsTools(BaseTool):
- resource_ids: List of UUIDs for cloud resources associated with this finding
Workflow:
- 1. Use prowler_app_search_security_findings to browse and filter findings
+ 1. Use prowler_search_security_findings to browse and filter findings
2. Use this tool with the finding 'id' to get remediation guidance and complete context
"""
params = {
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/integrations.py b/mcp_server/prowler_mcp_server/prowler_app/tools/integrations.py
new file mode 100644
index 0000000000..d0aac0182a
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/integrations.py
@@ -0,0 +1,1096 @@
+"""Integrations tools for Prowler MCP Server.
+
+This module provides tools for managing where Prowler sends its results, including:
+- Generic integration lifecycle (list, get, update, delete, connection check)
+- Integration creation, with one tool per integration type
+- Jira specific operations (available issue types, sending findings as work items)
+"""
+
+import json
+from typing import Any
+
+from pydantic import Field
+
+from prowler_mcp_server.prowler_app.models.integrations import (
+ DetailedIntegration,
+ IntegrationConnectionStatus,
+ IntegrationsListResponse,
+ JiraDispatchResult,
+ JiraIssueTypes,
+)
+from prowler_mcp_server.prowler_app.tools.base import BaseTool
+from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIError
+
+# The configuration is deliberately left out of the list view, it belongs to the
+# detailed view returned by prowler_get_integration
+INTEGRATION_LIST_FIELDS = (
+ "enabled,connected,connection_last_checked_at,integration_type,providers,"
+ "inserted_at,updated_at"
+)
+
+CONNECTION_CHECK_TIMEOUT = 120
+# One Jira work item is created per finding, sequentially, so this needs to be generous
+JIRA_DISPATCH_TIMEOUT = 300
+
+# The API replaces the whole credentials object, so a partial one destroys the rest
+JIRA_REQUIRED_CREDENTIALS = ("domain", "user_mail", "api_token")
+
+
+def _providers_relationship(provider_ids: list[str]) -> dict[str, Any]:
+ """Build the JSON:API relationship linkage attaching an integration to providers."""
+ return {
+ "providers": {
+ "data": [
+ {"type": "providers", "id": provider_id} for provider_id in provider_ids
+ ]
+ }
+ }
+
+
+class IntegrationsTools(BaseTool):
+ """Tools for integration management operations.
+
+ Provides tools for:
+ - prowler_list_integrations: List the configured integrations and their connection state
+ - prowler_get_integration: Get an integration with its full configuration
+ - prowler_create_amazon_s3_integration: Export scan outputs to an S3 bucket
+ - prowler_create_aws_security_hub_integration: Send findings to AWS Security Hub
+ - prowler_create_jira_integration: Connect a Jira site to open work items from findings
+ - prowler_update_integration: Change credentials, configuration, providers or enabled state
+ - prowler_delete_integration: Permanently remove an integration
+ - prowler_test_integration_connection: Check an integration connection and refresh its discovered configuration
+ - prowler_get_jira_issue_types: List the issue types available in a Jira project
+ - prowler_send_findings_to_jira: Create Jira work items for a set of findings
+ """
+
+ async def list_integrations(
+ self,
+ integration_type: list[str] = Field(
+ default=[],
+ description="Filter by integration type(s). Valid values: 'amazon_s3' (export scan outputs to an S3 bucket), 'aws_security_hub' (send findings to AWS Security Hub), 'jira' (open Jira work items from findings). Leave empty to return every type.",
+ ),
+ page_size: int = Field(
+ default=50, description="Number of results to return per page."
+ ),
+ page_number: int = Field(
+ default=1, description="Page number to retrieve (1-indexed)"
+ ),
+ ) -> dict[str, Any]:
+ """List the integrations configured in Prowler, with their connection state.
+
+ Integrations are the destinations Prowler sends its results to. They are configured
+ per tenant and require the 'manage_integrations' permission.
+
+ IMPORTANT: This tool returns LIGHTWEIGHT integrations without the integration-type
+ specific configuration. Use prowler_get_integration to get the full configuration,
+ such as the S3 bucket name or the available Jira projects.
+
+ Default behavior:
+ - Returns every integration type
+ - Returns 50 integrations per page. Tenants normally have a handful of them, so the
+ first page usually contains all of them
+
+ Each integration includes:
+ - Core identification: id (UUID for prowler_get_integration), integration_type
+ - State: enabled, connected (true, false, or null when never checked), connection_last_checked_at
+ - Scope: provider_ids, the providers the integration is attached to. Empty means it
+ applies to the whole tenant, which is always the case for Jira
+ - Temporal data: inserted_at, updated_at timestamps
+
+ NOTE: The API does not support filtering by 'enabled' or 'connected'. Read those
+ fields from the returned results instead.
+
+ Workflow:
+ 1. Use this tool to see which integrations exist and whether they are working
+ 2. Use prowler_get_integration with the 'id' to get the full configuration
+ 3. Use prowler_test_integration_connection to re-check a broken integration
+ 4. Use prowler_update_integration to fix credentials or settings
+ """
+ self.logger.info("Listing integrations...")
+ self.api_client.validate_page_size(page_size)
+
+ params = {
+ "fields[integrations]": INTEGRATION_LIST_FIELDS,
+ "page[size]": page_size,
+ "page[number]": page_number,
+ }
+
+ if integration_type:
+ params["filter[integration_type__in]"] = integration_type
+
+ clean_params = self.api_client.build_filter_params(params)
+ api_response = await self.api_client.get("/integrations", params=clean_params)
+
+ simplified_response = IntegrationsListResponse.from_api_response(api_response)
+ return simplified_response.model_dump()
+
+ async def get_integration(
+ self,
+ integration_id: str = Field(
+ description="UUID of the integration to retrieve. Must be a valid UUID format (e.g., '019ac0d6-90d5-73e9-9acf-c22e256f1bac'). Use prowler_list_integrations to find it."
+ ),
+ ) -> dict[str, Any]:
+ """Retrieve an integration with its complete, integration-type specific configuration.
+
+ IMPORTANT: Credentials are never returned by Prowler, only the configuration.
+
+ This tool provides ALL information that prowler_list_integrations returns PLUS the
+ 'configuration' object, whose contents depend on the integration type:
+ - amazon_s3: 'bucket_name' and 'output_directory'
+ - aws_security_hub: 'send_only_fails', 'archive_previous_findings' and
+ 'enabled_regions' (the AWS regions Security Hub is enabled in, discovered by the
+ connection check)
+ - jira: 'domain', 'projects' (a mapping of project key to project name) and
+ 'issue_types' (a mapping of project key to its available issue types). Both are
+ discovered by the connection check, so an empty 'projects' means the connection
+ has not been checked yet
+
+ Workflow:
+ 1. Use prowler_list_integrations to find the integration 'id'
+ 2. Use this tool to read its configuration
+ 3. For Jira, read 'projects' here before calling prowler_get_jira_issue_types
+ """
+ self.logger.info(f"Retrieving integration {integration_id}...")
+
+ integration = await self._get_integration_raw(integration_id)
+ return DetailedIntegration.from_api_response(integration).model_dump()
+
+ async def create_amazon_s3_integration(
+ self,
+ bucket_name: str = Field(
+ description="Name of the S3 bucket where Prowler will upload the scan outputs (CSV, HTML, OCSF JSON and compliance reports)."
+ ),
+ output_directory: str = Field(
+ default="output",
+ description='Directory inside the bucket where the outputs are written. Normalized server-side: leading slashes are stripped, the characters < > : " | ? * are rejected and the maximum length is 900 characters.',
+ ),
+ provider_ids: list[str] = Field(
+ default=[],
+ description="Prowler UUIDs of the providers whose scan outputs are exported to this bucket. Use prowler_search_providers to find them. Leave empty to attach no provider yet.",
+ ),
+ role_arn: str | None = Field(
+ default=None,
+ description="ARN of the IAM role Prowler assumes to write to the bucket (e.g. 'arn:aws:iam::123456789012:role/ProwlerS3Integration'). Recommended over static keys.",
+ ),
+ external_id: str | None = Field(
+ default=None,
+ description="External ID required by the trust policy of the assumed role. In Prowler Cloud this is the tenant ID.",
+ ),
+ role_session_name: str | None = Field(
+ default=None,
+ description="Identifier for the role session, useful to track it in AWS logs. Only letters, digits and the characters =,.@_- are allowed.",
+ ),
+ session_duration: int = Field(
+ default=3600,
+ description="Duration of the assumed role session in seconds. Must be between 900 and 43200. Defaults to 3600 when omitted.",
+ ),
+ aws_access_key_id: str | None = Field(
+ default=None,
+ description="AWS access key ID. Only needed when the Prowler deployment has no ambient AWS credentials.",
+ ),
+ aws_secret_access_key: str | None = Field(
+ default=None,
+ description="AWS secret access key. Required when 'aws_access_key_id' is provided.",
+ ),
+ aws_session_token: str | None = Field(
+ default=None,
+ description="AWS session token, only for temporary credentials.",
+ ),
+ enabled: bool = Field(
+ default=True,
+ description="Whether the integration starts enabled. A disabled integration is never used after a scan.",
+ ),
+ ) -> dict[str, Any]:
+ """Create an Amazon S3 integration to export scan outputs to an S3 bucket.
+
+ After every scan of an attached provider, Prowler uploads the generated reports to
+ 's3://{bucket_name}/{output_directory}/'.
+
+ IMPORTANT: The connection is checked right after creation, and the result is part of
+ the response. The check writes and deletes a small test object in the bucket, so the
+ credentials need s3:PutObject, s3:ListBucket and s3:DeleteObject on it.
+
+ Default behavior:
+ - The integration is created enabled
+ - All credential parameters are optional: providing none sends empty credentials,
+ which makes Prowler use the ambient AWS credentials of the deployment. That only
+ works on self-hosted Prowler, Prowler Cloud requires a role or static keys
+
+ Example Input:
+ - IAM role (recommended):
+ ```json
+ {
+ "bucket_name": "my-security-reports",
+ "output_directory": "prowler",
+ "provider_ids": ["019ac0d6-90d5-73e9-9acf-c22e256f1bac"],
+ "role_arn": "arn:aws:iam::123456789012:role/ProwlerS3Integration",
+ "external_id": "019ac0d6-90d5-73e9-9acf-c22e256f1bac"
+ }
+ ```
+ - Static credentials:
+ ```json
+ {
+ "bucket_name": "my-security-reports",
+ "aws_access_key_id": "AKIA...",
+ "aws_secret_access_key": "..."
+ }
+ ```
+
+ Workflow:
+ 1. Use prowler_search_providers to get the provider UUIDs to attach
+ 2. Use this tool to create the integration
+ 3. Read 'connected' in the response. If it is 'failed', read 'error', then fix the
+ bucket policy or the credentials with prowler_update_integration
+ """
+ self.logger.info(f"Creating Amazon S3 integration for bucket {bucket_name}...")
+
+ try:
+ credentials = self._build_aws_credentials(
+ role_arn=role_arn,
+ external_id=external_id,
+ role_session_name=role_session_name,
+ session_duration=session_duration,
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ aws_session_token=aws_session_token,
+ )
+
+ return await self._create_integration(
+ integration_type="amazon_s3",
+ configuration={
+ "bucket_name": bucket_name,
+ "output_directory": output_directory,
+ },
+ credentials=credentials,
+ provider_ids=provider_ids,
+ enabled=enabled,
+ )
+ except Exception as e:
+ self.logger.error(f"Amazon S3 integration creation failed: {e}")
+ return {"error": str(e), "status": "failed"}
+
+ async def create_aws_security_hub_integration(
+ self,
+ provider_id: str = Field(
+ description="Prowler UUID of the AWS provider whose findings are sent to Security Hub. It must be an AWS provider, and it can only have one Security Hub integration. Use prowler_search_providers with provider_type=['aws'] to find it."
+ ),
+ send_only_fails: bool = Field(
+ default=False,
+ description="When true, only findings with FAIL status are sent to Security Hub. When false, passed findings are sent too.",
+ ),
+ archive_previous_findings: bool = Field(
+ default=False,
+ description="When true, findings that are no longer present in the latest scan are archived in Security Hub.",
+ ),
+ role_arn: str | None = Field(
+ default=None,
+ description="ARN of a dedicated IAM role Prowler assumes to write to Security Hub. Leave every credential parameter empty to reuse the credentials already stored for the provider, which is the recommended setup.",
+ ),
+ external_id: str | None = Field(
+ default=None,
+ description="External ID required by the trust policy of the assumed role.",
+ ),
+ role_session_name: str | None = Field(
+ default=None,
+ description="Identifier for the role session, useful to track it in AWS logs. Only letters, digits and the characters =,.@_- are allowed.",
+ ),
+ session_duration: int | None = Field(
+ default=None,
+ description="Duration of the assumed role session in seconds. Must be between 900 and 43200. Defaults to 3600 when omitted.",
+ ),
+ aws_access_key_id: str | None = Field(
+ default=None, description="AWS access key ID for dedicated credentials."
+ ),
+ aws_secret_access_key: str | None = Field(
+ default=None,
+ description="AWS secret access key. Required when 'aws_access_key_id' is provided.",
+ ),
+ aws_session_token: str | None = Field(
+ default=None,
+ description="AWS session token, only for temporary credentials.",
+ ),
+ enabled: bool = Field(
+ default=True,
+ description="Whether the integration starts enabled. A disabled integration is never used after a scan.",
+ ),
+ ) -> dict[str, Any]:
+ """Create an AWS Security Hub integration to send findings to Security Hub in ASFF format.
+
+ After every scan of the attached provider, Prowler pushes its findings to Security Hub
+ in every region where the Prowler partner integration is enabled.
+
+ IMPORTANT: The Prowler integration must be enabled in AWS Security Hub beforehand, in
+ each region where findings should land. The connection check performed right after
+ creation is what discovers those regions and fills 'enabled_regions'.
+
+ Default behavior:
+ - The integration is created enabled
+ - Leaving every credential parameter empty makes Prowler reuse the credentials already
+ stored for the provider. This is the recommended setup
+ - send_only_fails defaults to false, so passed findings are sent too
+
+ Constraints:
+ - Exactly one provider, and it must be an AWS provider
+ - A provider can only have one Security Hub integration. Creating a second one fails
+ with a conflict error
+
+ Workflow:
+ 1. Use prowler_search_providers with provider_type=['aws'] to get the provider UUID
+ 2. Use this tool to create the integration
+ 3. Read 'enabled_regions' in the response configuration. If it is empty, the Prowler
+ integration is not enabled in Security Hub yet
+ """
+ self.logger.info(
+ f"Creating AWS Security Hub integration for provider {provider_id}..."
+ )
+
+ try:
+ credentials = self._build_aws_credentials(
+ role_arn=role_arn,
+ external_id=external_id,
+ role_session_name=role_session_name,
+ session_duration=session_duration,
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ aws_session_token=aws_session_token,
+ )
+
+ return await self._create_integration(
+ integration_type="aws_security_hub",
+ configuration={
+ "send_only_fails": send_only_fails,
+ "archive_previous_findings": archive_previous_findings,
+ },
+ credentials=credentials,
+ provider_ids=[provider_id],
+ enabled=enabled,
+ )
+ except Exception as e:
+ self.logger.error(f"AWS Security Hub integration creation failed: {e}")
+ return {"error": str(e), "status": "failed"}
+
+ async def create_jira_integration(
+ self,
+ domain: str = Field(
+ description="Atlassian site name, without the '.atlassian.net' suffix. For the site 'https://acme.atlassian.net' the value is 'acme'. Full URLs are accepted and normalized automatically."
+ ),
+ user_mail: str = Field(
+ description="Email address of the Atlassian account that owns the API token."
+ ),
+ api_token: str = Field(
+ description="Atlassian API token, created from the account settings. It needs the 'read:jira-user', 'read:jira-work' and 'write:jira-work' scopes."
+ ),
+ enabled: bool = Field(
+ default=True,
+ description="Whether the integration starts enabled. Findings cannot be sent to a disabled Jira integration.",
+ ),
+ ) -> dict[str, Any]:
+ """Create a Jira integration to open Jira work items from Prowler findings.
+
+ Unlike the other integration types, Jira is tenant-wide: it is not attached to any
+ provider and applies to every finding the role can see.
+
+ IMPORTANT: Jira integrations do not send anything automatically. Work items are only
+ created on demand with prowler_send_findings_to_jira.
+
+ IMPORTANT: The connection is checked right after creation, and that check is what
+ discovers the available Jira projects. If 'connected' comes back 'failed', the
+ 'projects' mapping stays empty and no finding can be dispatched.
+
+ Default behavior:
+ - The integration is created enabled
+ - The configuration is entirely server-generated: 'domain', 'projects' and 'issue_types'
+
+ Example Input:
+ ```json
+ {
+ "domain": "acme",
+ "user_mail": "security@acme.com",
+ "api_token": "ATATT3xFfGF0..."
+ }
+ ```
+
+ Workflow:
+ 1. Use this tool to create the integration
+ 2. Read 'projects' in the response configuration to pick a project key
+ 3. Use prowler_get_jira_issue_types with that project key to pick an issue type
+ 4. Use prowler_send_findings_to_jira to create the work items
+ """
+ try:
+ normalized_domain = self._normalize_atlassian_domain(domain)
+ self.logger.info(
+ f"Creating Jira integration for domain {normalized_domain}..."
+ )
+
+ return await self._create_integration(
+ integration_type="jira",
+ # Jira rejects any configuration in the payload, the API generates it
+ configuration={},
+ credentials={
+ "domain": normalized_domain,
+ "user_mail": user_mail,
+ "api_token": api_token,
+ },
+ provider_ids=[],
+ enabled=enabled,
+ )
+ except Exception as e:
+ self.logger.error(f"Jira integration creation failed: {e}")
+ return {"error": str(e), "status": "failed"}
+
+ async def update_integration(
+ self,
+ integration_id: str = Field(
+ description="UUID of the integration to update. Use prowler_list_integrations to find it."
+ ),
+ enabled: bool | None = Field(
+ default=None,
+ description="Enable (True) or disable (False) the integration. If not specified, the enabled state remains unchanged.",
+ ),
+ provider_ids: list[str] | None = Field(
+ default=None,
+ description="Replace the providers this integration is attached to. Omit to keep the current ones. For 'amazon_s3' an empty list detaches every provider. For 'aws_security_hub' exactly one provider ID is required, since the integration cannot exist without one. Not accepted for Jira integrations, which are tenant-wide.",
+ ),
+ configuration: (
+ dict[str, Any] | str | None
+ ) = Field( # `str` accepted due to bad MCP Clients implementation
+ default=None,
+ description="Integration-type specific settings to change. Only the keys provided are modified, the rest of the configuration is preserved. For 'amazon_s3': 'bucket_name', 'output_directory'. For 'aws_security_hub': 'send_only_fails', 'archive_previous_findings'. Not accepted for 'jira', whose configuration is entirely server-generated.",
+ ),
+ credentials: (
+ dict[str, Any] | str | None
+ ) = Field( # `str` accepted due to bad MCP Clients implementation
+ default=None,
+ description="Replace the stored credentials. The whole object is replaced, so every needed key must be provided. For 'amazon_s3' and 'aws_security_hub': any of 'role_arn', 'external_id', 'role_session_name', 'session_duration', 'aws_access_key_id', 'aws_secret_access_key', 'aws_session_token'; an empty object clears them so Prowler falls back to the ambient or provider credentials. For 'jira': 'domain', 'user_mail' and 'api_token', all required, an empty or partial object is refused because it would destroy the stored credentials.",
+ ),
+ ) -> dict[str, Any]:
+ """Update an integration's credentials, configuration, providers or enabled state.
+
+ The integration type cannot be changed. To switch types, delete the integration and
+ create a new one.
+
+ Default behavior:
+ - Only the parameters provided are changed, everything else is preserved
+ - 'configuration' is merged with the current one, so partial updates are safe
+ - When 'credentials', 'configuration' or the attached providers change, the connection
+ is re-checked and the result is part of the response. Toggling only 'enabled' does
+ not re-check it
+
+ Constraints:
+ - Jira integrations reject 'configuration' and 'provider_ids'. Sending a configuration
+ would wipe the discovered 'projects' and 'issue_types', so this tool refuses it
+ - Jira 'credentials' are replaced as a whole, so 'domain', 'user_mail' and 'api_token'
+ are all required. An empty or partial object is refused because it would destroy the
+ stored credentials
+ - Security Hub integrations must keep exactly one AWS provider, so 'provider_ids' has
+ to contain a single ID. Use prowler_delete_integration to stop sending findings
+ - The 'enabled_regions' of a Security Hub integration are server-owned and cannot be
+ set here, they are refreshed by the connection check
+
+ Workflow:
+ 1. Use prowler_get_integration to read the current configuration
+ 2. Use this tool with only the fields to change
+ 3. Read 'connected' in the response to confirm the integration still works
+ """
+ self.logger.info(f"Updating integration {integration_id}...")
+
+ try:
+ current = DetailedIntegration.from_api_response(
+ await self._get_integration_raw(integration_id)
+ )
+ integration_type = current.integration_type
+
+ if provider_ids is not None:
+ if integration_type == "jira":
+ raise ValueError(
+ "Jira integrations are tenant-wide and cannot be attached to providers."
+ )
+ if integration_type == "aws_security_hub" and len(provider_ids) != 1:
+ raise ValueError(
+ "AWS Security Hub integrations must stay attached to exactly one AWS "
+ f"provider, got {len(provider_ids)}. Pass a single provider ID, or use "
+ "prowler_delete_integration to stop sending findings to Security Hub."
+ )
+
+ attributes: dict[str, Any] = {}
+ if enabled is not None:
+ attributes["enabled"] = enabled
+
+ if credentials is not None:
+ attributes["credentials"] = self._validate_credentials(
+ integration_type, self._as_dict(credentials, "credentials")
+ )
+
+ if configuration is not None:
+ if integration_type == "jira":
+ raise ValueError(
+ "Jira integrations do not accept a configuration: it is generated by Prowler. "
+ "Update the credentials instead, or run prowler_test_integration_connection to "
+ "refresh the available projects and issue types."
+ )
+ merged = dict(current.configuration)
+ merged.update(self._as_dict(configuration, "configuration"))
+ # Server-owned, the API repopulates it from the connection check
+ merged.pop("regions", None)
+ merged.pop("enabled_regions", None)
+ attributes["configuration"] = merged
+
+ if not attributes and provider_ids is None:
+ self.logger.info("No changes provided, returning the current state")
+ return current.model_dump()
+
+ update_body: dict[str, Any] = {
+ "data": {
+ "type": "integrations",
+ "id": integration_id,
+ "attributes": attributes,
+ }
+ }
+ if provider_ids is not None:
+ update_body["data"]["relationships"] = _providers_relationship(
+ provider_ids
+ )
+
+ await self.api_client.patch(
+ f"/integrations/{integration_id}", json_data=update_body
+ )
+
+ # A different provider means different effective credentials and different
+ # discovered configuration, so the stored connection state is stale too
+ providers_changed = provider_ids is not None and set(provider_ids) != set(
+ current.provider_ids
+ )
+ recheck_connection = (
+ credentials is not None
+ or configuration is not None
+ or providers_changed
+ )
+ connection_status = (
+ await self._test_connection(integration_id)
+ if recheck_connection
+ else None
+ )
+
+ updated = await self._get_integration_raw(integration_id)
+ if connection_status is not None:
+ return IntegrationConnectionStatus.create(
+ updated, connection_status
+ ).model_dump()
+ return DetailedIntegration.from_api_response(updated).model_dump()
+ except Exception as e:
+ self.logger.error(f"Integration update failed: {e}")
+ return {"error": str(e), "status": "failed"}
+
+ async def delete_integration(
+ self,
+ integration_id: str = Field(
+ description="UUID of the integration to permanently remove. Use prowler_list_integrations to find it."
+ ),
+ ) -> dict[str, Any]:
+ """Permanently remove an integration from Prowler.
+
+ WARNING: This is a destructive operation that cannot be undone. The stored credentials
+ are destroyed with it, so the integration has to be recreated from scratch, with its
+ credentials, to be used again.
+
+ Deletion behavior:
+ - Prowler stops sending results to this destination immediately
+ - Data already exported stays where it is: objects in S3, findings in Security Hub and
+ work items in Jira are not removed
+ - To pause an integration instead, use prowler_update_integration with enabled=False
+
+ Workflow:
+ 1. Use prowler_get_integration to review what will be deleted
+ 2. Use this tool to permanently remove it
+ 3. Verify with prowler_list_integrations (it should no longer appear)
+ """
+ self.logger.info(f"Deleting integration {integration_id}...")
+
+ try:
+ await self.api_client.delete(f"/integrations/{integration_id}")
+ return {
+ "deleted": True,
+ "message": f"Integration {integration_id} deleted successfully",
+ }
+ except Exception as e:
+ self.logger.error(f"Integration deletion failed: {e}")
+ return {
+ "deleted": False,
+ "message": f"Integration {integration_id} deletion failed: {str(e)}",
+ }
+
+ async def test_integration_connection(
+ self,
+ integration_id: str = Field(
+ description="UUID of the integration to check. Use prowler_list_integrations to find it."
+ ),
+ ) -> dict[str, Any]:
+ """Check that Prowler can reach an integration with its stored credentials.
+
+ This also refreshes the parts of the configuration that Prowler discovers from the
+ remote system, so it is the way to repair a stale configuration:
+ - jira: repopulates 'projects' and 'issue_types'
+ - aws_security_hub: repopulates 'enabled_regions'
+
+ IMPORTANT: A disabled integration is never checked. It comes back as 'failed' with the
+ error 'Integration is not enabled'. Enable it first with prowler_update_integration.
+
+ The check runs as a background task and this tool waits for it, so it can take a few
+ seconds to return.
+
+ Workflow:
+ 1. Use prowler_list_integrations to spot integrations with connected=false
+ 2. Use this tool to re-check one after fixing its permissions on the remote side
+ 3. If it still fails, read 'error' and fix the credentials with prowler_update_integration
+ """
+ self.logger.info(f"Checking connection of integration {integration_id}...")
+
+ connection_status = await self._test_connection(integration_id)
+ integration = await self._get_integration_raw(integration_id)
+
+ return IntegrationConnectionStatus.create(
+ integration, connection_status
+ ).model_dump()
+
+ async def get_jira_issue_types(
+ self,
+ integration_id: str = Field(
+ description="UUID of the Jira integration. Use prowler_list_integrations with integration_type=['jira'] to find it."
+ ),
+ project_key: str = Field(
+ description="Key of the Jira project to read the issue types from (e.g. 'PROJ'). It must be one of the keys in the 'projects' mapping of the integration configuration."
+ ),
+ ) -> dict[str, Any]:
+ """List the issue types available in a Jira project.
+
+ Prowler fetches them live from Jira and stores them in the integration configuration,
+ so the answer is always current.
+
+ IMPORTANT: The project key must already be present in the 'projects' mapping of the
+ integration configuration. That mapping is discovered by the connection check, so run
+ prowler_test_integration_connection first if it is empty.
+
+ NOTE: Issue types that require custom fields Prowler does not fill, such as Epic, will
+ be listed here but fail when actually creating the work item. Prefer Task, Bug or Story.
+
+ Workflow:
+ 1. Use prowler_get_integration to read the 'projects' mapping and pick a project key
+ 2. Use this tool to get the valid issue types for that project
+ 3. Use prowler_send_findings_to_jira with the chosen project key and issue type
+ """
+ self.logger.info(
+ f"Fetching Jira issue types of project {project_key} for integration {integration_id}..."
+ )
+
+ api_response = await self.api_client.get(
+ f"/integrations/{integration_id}/jira/issue_types",
+ params={"project_key": project_key},
+ )
+
+ issue_types = JiraIssueTypes.from_api_response(api_response.get("data", {}))
+ return issue_types.model_dump()
+
+ async def send_findings_to_jira(
+ self,
+ integration_id: str = Field(
+ description="UUID of the Jira integration to send the findings through. It must be enabled."
+ ),
+ project_key: str = Field(
+ description="Key of the Jira project the work items are created in (e.g. 'PROJ'). It must be one of the keys in the 'projects' mapping of the integration configuration."
+ ),
+ issue_type: str = Field(
+ description="Jira issue type for the created work items (e.g. 'Task', 'Bug', 'Story'). It must be one of the values returned by prowler_get_jira_issue_types for this project."
+ ),
+ finding_ids: list[str] = Field(
+ description="UUIDs of the findings to send. One Jira work item is created per finding. Get them from prowler_search_security_findings. Must contain at least one ID."
+ ),
+ ) -> dict[str, Any]:
+ """Create Jira work items for a set of findings.
+
+ Each work item carries the finding's check title, severity, status, provider, region,
+ resource, risk description and remediation steps.
+
+ WARNING: This creates real work items in Jira. Prowler cannot delete or update them
+ afterwards, they have to be handled in Jira. Only call this again for the same findings
+ when the previous response had safe_to_retry=true, otherwise it creates duplicates.
+
+ WARNING: Avoid issue types that require custom fields Prowler does not fill, such as
+ Epic. Creation fails for those. Task, Bug and Story normally work.
+
+ Default behavior:
+ - One work item per finding, created sequentially, so large batches take a while
+ - The dispatch runs as a background task and this tool waits up to 5 minutes for it.
+ If it is still running by then, the response has status='in_progress', an 'error'
+ explaining it, and the 'task_id'
+
+ The result includes:
+ - status: 'completed' when Prowler finished the dispatch, 'in_progress' when the task
+ is still running, 'failed' when the dispatch was rejected before it started,
+ 'unknown' when the task stopped without reporting a result
+ - safe_to_retry: whether the dispatch can be sent again. It is only true when no work
+ item was created, which is the case when the dispatch was rejected before it
+ started. NEVER call this tool again for the same findings when it is false, the
+ work items already created would be duplicated. Report the outcome to the user and
+ let them check Jira instead
+ - created_count: number of work items created in Jira, absent unless status='completed'
+ - failed_count: number of findings that could not be sent, absent unless
+ status='completed'
+
+ Workflow:
+ 1. Use prowler_search_security_findings to select the findings to escalate
+ 2. Use prowler_get_integration to read the 'projects' mapping and pick a project key
+ 3. Use prowler_get_jira_issue_types to pick a valid issue type
+ 4. Use this tool with the finding IDs
+ """
+ try:
+ if not finding_ids:
+ raise ValueError(
+ "At least one finding ID is required. Use prowler_search_security_findings to get them."
+ )
+
+ self.logger.info(
+ f"Sending {len(finding_ids)} finding(s) to Jira project {project_key}..."
+ )
+
+ dispatch_body = {
+ "data": {
+ "type": "integrations-jira-dispatches",
+ "attributes": {
+ "project_key": project_key,
+ "issue_type": issue_type,
+ },
+ }
+ }
+ params = self.api_client.build_filter_params(
+ {"filter[finding_id__in]": finding_ids}
+ )
+
+ task_response = await self.api_client.post(
+ f"/integrations/{integration_id}/jira/dispatches",
+ params=params,
+ json_data=dispatch_body,
+ )
+ except ValueError as e:
+ # Refused here, so the request never went out
+ self.logger.error(f"Jira dispatch was refused before the request: {e}")
+ return self._jira_dispatch_rejected(str(e))
+ except ProwlerAPIError as e:
+ # Only a client error is a refusal: the API validates the dispatch and
+ # then queues the background task before serializing its answer, so a
+ # server error may well come back with work items already being created
+ if e.status_code >= 500:
+ self.logger.error(f"Jira dispatch failed on the server: {e}")
+ return self._jira_dispatch_unknown(
+ task_id=None,
+ error=(
+ f"the request that starts the dispatch failed on the server: {e} "
+ "It may have been queued anyway."
+ ),
+ )
+
+ self.logger.error(f"Jira dispatch was rejected by Prowler: {e}")
+ return self._jira_dispatch_rejected(str(e))
+ except Exception as e:
+ # No answer came back, so the request may still have been accepted
+ self.logger.error(f"Jira dispatch could not be started: {e}")
+ return self._jira_dispatch_unknown(
+ task_id=None,
+ error=(
+ f"the request that starts the dispatch got no answer: {e} "
+ "It may have been accepted anyway."
+ ),
+ )
+
+ task_id = task_response.get("data", {}).get("id")
+ if not task_id:
+ self.logger.error("Jira dispatch response did not include a task ID")
+ return self._jira_dispatch_unknown(
+ task_id=None,
+ error="Prowler accepted the dispatch but did not return the ID of the background task, so its outcome cannot be checked.",
+ )
+
+ try:
+ completed_task = await self.api_client.poll_task_until_complete(
+ task_id=task_id, timeout=JIRA_DISPATCH_TIMEOUT, poll_interval=2.0
+ )
+ except Exception as e:
+ self.logger.error(f"Jira dispatch did not complete cleanly: {e}")
+ return await self._jira_dispatch_fallback(task_id, str(e))
+
+ try:
+ return JiraDispatchResult.from_task_result(
+ completed_task.get("data", {}).get("attributes", {}).get("result")
+ ).model_dump()
+ except ValueError as e:
+ self.logger.error(f"Jira dispatch result could not be read: {e}")
+ return self._jira_dispatch_unknown(task_id, str(e))
+
+ # Private helper methods
+
+ def _build_aws_credentials(
+ self,
+ role_arn: str | None = None,
+ external_id: str | None = None,
+ role_session_name: str | None = None,
+ session_duration: int | None = None,
+ aws_access_key_id: str | None = None,
+ aws_secret_access_key: str | None = None,
+ aws_session_token: str | None = None,
+ ) -> dict[str, Any]:
+ """Build the AWS credentials object, leaving out the values not provided.
+
+ An empty result is valid: it makes Prowler fall back to the ambient AWS credentials
+ of the deployment, or to the credentials stored for the provider in the case of
+ Security Hub.
+ """
+ credentials = {
+ "role_arn": role_arn,
+ "external_id": external_id,
+ "role_session_name": role_session_name,
+ "session_duration": session_duration,
+ "aws_access_key_id": aws_access_key_id,
+ "aws_secret_access_key": aws_secret_access_key,
+ "aws_session_token": aws_session_token,
+ }
+ return {key: value for key, value in credentials.items() if value is not None}
+
+ def _normalize_atlassian_domain(self, domain: str) -> str:
+ """Reduce a Jira site URL to the bare Atlassian site name.
+
+ The API only accepts the site name, so 'https://acme.atlassian.net/jira' has to be
+ sent as 'acme'.
+ """
+ normalized = domain.strip()
+ normalized = normalized.split("://", 1)[-1]
+ normalized = normalized.split("/", 1)[0]
+ normalized = normalized.removesuffix(".atlassian.net")
+
+ if not normalized:
+ raise ValueError(
+ f"Invalid Jira domain: {domain}. Provide the Atlassian site name, for example "
+ "'acme' for the site 'https://acme.atlassian.net'."
+ )
+ return normalized
+
+ def _validate_credentials(
+ self, integration_type: str, credentials: dict[str, Any]
+ ) -> dict[str, Any]:
+ """Check that replacing the credentials leaves the integration usable.
+
+ The API replaces the stored credentials with whatever is sent, so an empty or partial
+ object silently destroys them. That is only acceptable for the AWS integration types,
+ where no credentials means falling back to the ambient or provider ones.
+ """
+ if integration_type != "jira":
+ return credentials
+
+ missing = [
+ key
+ for key in JIRA_REQUIRED_CREDENTIALS
+ if not isinstance(credentials.get(key), str) or not credentials[key].strip()
+ ]
+ if missing:
+ raise ValueError(
+ "Jira credentials are replaced as a whole, so 'domain', 'user_mail' and "
+ f"'api_token' are all required. Missing or empty: {', '.join(missing)}. "
+ "Sending an incomplete object would destroy the stored credentials and break "
+ "the integration."
+ )
+
+ return {
+ **credentials,
+ "domain": self._normalize_atlassian_domain(credentials["domain"]),
+ }
+
+ def _as_dict(self, value: dict[str, Any] | str, param_name: str) -> dict[str, Any]:
+ """Accept a JSON object sent as a string by clients that cannot pass objects."""
+ if isinstance(value, str):
+ try:
+ value = json.loads(value)
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Invalid JSON for {param_name}: {e}")
+
+ if not isinstance(value, dict):
+ raise ValueError(f"{param_name} must be a JSON object.")
+ return value
+
+ async def _get_integration_raw(self, integration_id: str) -> dict[str, Any]:
+ """Fetch the raw JSON:API resource of an integration.
+
+ Raises:
+ ValueError: If the payload does not contain a usable integration resource
+ """
+ response = await self.api_client.get(f"/integrations/{integration_id}")
+ integration = response.get("data")
+
+ if not isinstance(integration, dict) or not integration.get("id"):
+ raise ValueError(
+ f"Integration {integration_id} was not found. Use prowler_list_integrations "
+ "to get a valid integration ID."
+ )
+
+ if not isinstance(integration.get("attributes"), dict):
+ raise ValueError(
+ f"Prowler returned integration {integration_id} without its attributes, so "
+ "its state cannot be read."
+ )
+
+ return integration
+
+ async def _create_integration(
+ self,
+ integration_type: str,
+ configuration: dict[str, Any],
+ credentials: dict[str, Any],
+ provider_ids: list[str],
+ enabled: bool,
+ ) -> dict[str, Any]:
+ """Create an integration and report the outcome of its connection check.
+
+ The check is always run: for Jira and Security Hub it is what discovers the projects
+ and the enabled regions, so without it the integration is not usable.
+ """
+ create_body: dict[str, Any] = {
+ "data": {
+ "type": "integrations",
+ "attributes": {
+ "integration_type": integration_type,
+ "configuration": configuration,
+ "credentials": credentials,
+ "enabled": enabled,
+ },
+ }
+ }
+ if provider_ids:
+ create_body["data"]["relationships"] = _providers_relationship(provider_ids)
+
+ api_response = await self.api_client.post(
+ "/integrations", json_data=create_body
+ )
+ integration_id = api_response.get("data", {}).get("id")
+
+ if not integration_id:
+ raise ValueError(
+ "Prowler accepted the integration creation but did not return its ID, so the "
+ "connection could not be checked. Use prowler_list_integrations to see whether "
+ "the integration exists before creating it again."
+ )
+
+ connection_status = await self._test_connection(integration_id)
+
+ try:
+ integration = await self._get_integration_raw(integration_id)
+ except Exception as e:
+ # The integration exists, so surface its ID instead of a plain read failure
+ raise ValueError(
+ f"Integration {integration_id} was created, but reading its state failed: {e} "
+ "Use prowler_get_integration with that ID to check it."
+ ) from e
+
+ return IntegrationConnectionStatus.create(
+ integration, connection_status
+ ).model_dump()
+
+ async def _test_connection(self, integration_id: str) -> dict[str, Any]:
+ """Run the connection check of an integration and wait for its result.
+
+ A check that could not be run is reported as 'connected: None' rather than a failure:
+ a disabled integration or wrong credentials come back as a completed task with
+ 'connected: False', so an exception here only means the outcome is unknown.
+
+ Returns:
+ Connection status dictionary with a 'connected' boolean or None, and an optional
+ 'error'
+ """
+ self.logger.info(f"Testing connection for integration {integration_id}...")
+ try:
+ task_response = await self.api_client.post(
+ f"/integrations/{integration_id}/connection", json_data={}
+ )
+ task_id = task_response.get("data", {}).get("id")
+
+ if not task_id:
+ raise ValueError(
+ "Prowler did not return the ID of the connection check task."
+ )
+
+ completed_task = await self.api_client.poll_task_until_complete(
+ task_id=task_id, timeout=CONNECTION_CHECK_TIMEOUT, poll_interval=1.0
+ )
+ result = completed_task.get("data", {}).get("attributes", {}).get("result")
+
+ if not isinstance(result, dict):
+ raise ValueError(
+ "The connection check task completed without reporting a result."
+ )
+
+ return result
+ except Exception as e:
+ self.logger.error(f"Connection check could not be completed: {e}")
+ return {
+ "connected": None,
+ "error": (
+ f"The connection check could not be completed: {e} This says nothing "
+ "about the stored credentials, run prowler_test_integration_connection "
+ "to check them again."
+ ),
+ }
+
+ async def _jira_dispatch_fallback(self, task_id: str, error: str) -> dict[str, Any]:
+ """Report a Jira dispatch whose polling did not end on a completed task.
+
+ The dispatch is never safe to retry here. Work items are created one by one, so a task
+ that failed or was cancelled halfway may already have created some of them, and a task
+ that is still running is creating them right now. The task state only decides how the
+ outcome is described.
+ """
+ state = None
+ try:
+ task = await self.api_client.get(f"/tasks/{task_id}")
+ state = task.get("data", {}).get("attributes", {}).get("state")
+ except Exception as e:
+ self.logger.error(f"Could not read the state of task {task_id}: {e}")
+
+ if state in ("failed", "cancelled"):
+ return self._jira_dispatch_unknown(
+ task_id,
+ f"The dispatch task ended as '{state}' before reporting a result. "
+ f"Original error: {error}",
+ )
+
+ return JiraDispatchResult(
+ status="in_progress",
+ safe_to_retry=False,
+ error=(
+ f"The dispatch is still running, so some work items may already exist in Jira. "
+ f"Do not send these findings again. Original error: {error}"
+ ),
+ task_id=task_id,
+ ).model_dump()
+
+ def _jira_dispatch_rejected(self, error: str) -> dict[str, Any]:
+ """Report a dispatch that was refused before any work item could be created.
+
+ This is the only outcome safe to retry, and it is reserved for the failures
+ that prove nothing was queued: a validation error raised here, or a client
+ error from the API, which rejects the dispatch before starting its task.
+ """
+ return JiraDispatchResult(
+ status="failed", safe_to_retry=True, error=error
+ ).model_dump()
+
+ def _jira_dispatch_unknown(self, task_id: str | None, error: str) -> dict[str, Any]:
+ """Report a dispatch whose outcome Prowler cannot determine.
+
+ Work items are created one by one, so an outcome that cannot be read is never safe to
+ retry: the dispatch may have created any number of them before stopping.
+ """
+ return JiraDispatchResult(
+ status="unknown",
+ safe_to_retry=False,
+ error=(
+ f"Prowler cannot tell how many Jira work items were created: {error} "
+ "Check the Jira project before sending these findings again."
+ ),
+ task_id=task_id,
+ ).model_dump()
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/muting.py b/mcp_server/prowler_mcp_server/prowler_app/tools/muting.py
index 639f1ec3b7..37e1504165 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/muting.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/muting.py
@@ -1,4 +1,4 @@
-"""Muting tools for Prowler App MCP Server.
+"""Muting tools for Prowler MCP Server.
This module provides tools for managing finding muting in Prowler, including:
- Mutelist management (pattern-based bulk muting)
@@ -43,7 +43,7 @@ class MutingTools(BaseTool):
Workflow:
1. Use this tool to check if a mutelist is configured
2. Examine current muting patterns before making updates
- 3. Use prowler_app_set_mutelist to create or update the configuration
+ 3. Use prowler_set_mutelist to create or update the configuration
"""
self.logger.info("Retrieving mutelist configuration...")
@@ -61,7 +61,7 @@ class MutingTools(BaseTool):
if len(data) == 0:
return {
"error": "No mutelist found",
- "message": "No mutelist configuration exists for this tenant. Use prowler_app_set_mutelist to create one.",
+ "message": "No mutelist configuration exists for this tenant. Use prowler_set_mutelist to create one.",
}
# Return the first (and only) mutelist
@@ -116,10 +116,10 @@ Structure:
- Exceptions: Accounts, Regions, Resources to exclude from muting
Workflow:
- 1. Use prowler_app_get_mutelist to check existing configuration
+ 1. Use prowler_get_mutelist to check existing configuration
2. Build configuration object following Prowler mutelist format
3. Use this tool to create or update the mutelist
- 4. Verify with prowler_app_get_mutelist
+ 4. Verify with prowler_get_mutelist
"""
self.logger.info("Setting mutelist configuration...")
@@ -171,12 +171,12 @@ Structure:
"""Remove the mutelist configuration from the tenant.
WARNING: This is a destructive operation that cannot be undone.
- - The mutelist will need to be re-created with prowler_app_set_mutelist
+ - The mutelist will need to be re-created with prowler_set_mutelist
- New findings from future scans will NOT be muted by the deleted mutelist
- Previously muted findings remain muted (deletion doesn't un-mute them)
Workflow:
- 1. Use prowler_app_get_mutelist to confirm what will be deleted
+ 1. Use prowler_get_mutelist to confirm what will be deleted
2. Use this tool to permanently remove the mutelist
3. New scans will no longer apply mutelist-based muting
"""
@@ -229,7 +229,7 @@ Structure:
"""Search and filter mute rules with pagination support.
IMPORTANT: This tool returns LIGHTWEIGHT mute rules without the full list of finding UIDs.
- Use prowler_app_get_mute_rule to get complete details including all finding UIDs and creator information.
+ Use prowler_get_mute_rule to get complete details including all finding UIDs and creator information.
Default behavior:
- Returns all mute rules (both enabled and disabled)
@@ -237,15 +237,15 @@ Structure:
- Includes basic rule information without full finding UID lists
Each mute rule includes:
- - Core identification: id (UUID for prowler_app_get_mute_rule), name
+ - Core identification: id (UUID for prowler_get_mute_rule), name
- Contextual information: reason, enabled status
- State tracking: finding_count (number of findings currently muted)
- Temporal data: inserted_at, updated_at timestamps
Workflow:
1. Use this tool to search and filter mute rules by name, enabled status, or keywords
- 2. Use prowler_app_get_mute_rule with the mute rule 'id' to get complete details including all finding UIDs
- 3. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify rules
+ 2. Use prowler_get_mute_rule with the mute rule 'id' to get complete details including all finding UIDs
+ 3. Use prowler_update_mute_rule or prowler_delete_mute_rule to modify rules
"""
self.logger.info("Listing mute rules...")
self.api_client.validate_page_size(page_size)
@@ -289,17 +289,17 @@ Structure:
"""Retrieve comprehensive details about a specific mute rule by its ID.
IMPORTANT: This tool returns COMPLETE mute rule details including the full list of finding UIDs.
- Use this after finding a rule via prowler_app_list_mute_rules.
+ Use this after finding a rule via prowler_list_mute_rules.
- This tool provides ALL information that prowler_app_list_mute_rules returns PLUS:
+ This tool provides ALL information that prowler_list_mute_rules returns PLUS:
- finding_uids: Complete list of finding UIDs that are muted by this rule
- user_creator_id: UUID of the user who created the rule (audit trail)
Workflow:
- 1. Use prowler_app_list_mute_rules to find rules by name or filter criteria
+ 1. Use prowler_list_mute_rules to find rules by name or filter criteria
2. Use this tool with the rule 'id' to get complete details
3. Examine finding_uids list to understand which findings are muted
- 4. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify if needed
+ 4. Use prowler_update_mute_rule or prowler_delete_mute_rule to modify if needed
"""
self.logger.info(f"Retrieving mute rule {rule_id}...")
@@ -323,7 +323,7 @@ Structure:
description="Reason for muting these findings. Document why this security issue is acceptable or intentional (e.g., 'Development environment with controlled access', 'Legacy application requires IMDSv1')."
),
finding_ids: list[str] = Field(
- description="List of finding IDs (UUIDs) to mute. Get these from the prowler_app_search_security_findings tool. Must provide at least 1 finding ID."
+ description="List of finding IDs (UUIDs) to mute. Get these from the prowler_search_security_findings tool. Must provide at least 1 finding ID."
),
) -> dict[str, Any]:
"""Create a new mute rule to mute specific findings with documentation and audit trail.
@@ -337,15 +337,15 @@ Structure:
- Records creator for audit trail
The mute rule includes:
- - Core identification: id (UUID for prowler_app_get_mute_rule), name, reason
+ - Core identification: id (UUID for prowler_get_mute_rule), name, reason
- Configuration: enabled status, finding_uids list
- Audit trail: user_creator_id (UUID of the Prowler user from the tenant that created the rule), timestamps when the rule was created and last modified
Workflow:
- 1. Use prowler_app_search_security_findings to identify findings to mute
+ 1. Use prowler_search_security_findings to identify findings to mute
2. Use this tool with finding IDs, descriptive name, and documented reason
- 3. Verify with prowler_app_get_mute_rule to confirm rule creation
- 4. Check findings are muted with prowler_app_search_security_findings (filter by muted=true)
+ 3. Verify with prowler_get_mute_rule to confirm rule creation
+ 4. Check findings are muted with prowler_search_security_findings (filter by muted=true)
"""
self.logger.info(f"Creating mute rule '{name}'...")
@@ -399,9 +399,9 @@ Structure:
- enabled: Toggle rule active status (doesn't affect already-muted findings)
Workflow:
- 1. Use prowler_app_get_mute_rule to see current rule state
+ 1. Use prowler_get_mute_rule to see current rule state
2. Use this tool to update name, reason, or enabled status
- 3. Verify changes with prowler_app_get_mute_rule
+ 3. Verify changes with prowler_get_mute_rule
"""
self.logger.info(f"Updating mute rule {rule_id}...")
@@ -451,9 +451,9 @@ Structure:
- Cannot be undone - rule must be recreated to restore
Workflow:
- 1. Use prowler_app_get_mute_rule to review what will be deleted
+ 1. Use prowler_get_mute_rule to review what will be deleted
2. Use this tool to permanently remove the rule
- 3. Verify deletion with prowler_app_list_mute_rules (rule should no longer appear)
+ 3. Verify deletion with prowler_list_mute_rules (rule should no longer appear)
"""
self.logger.info(f"Deleting mute rule {rule_id}...")
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/providers.py b/mcp_server/prowler_mcp_server/prowler_app/tools/providers.py
index b22d57d7b9..3ba417d677 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/providers.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/providers.py
@@ -1,4 +1,4 @@
-"""Provider Management tools for Prowler App MCP Server.
+"""Provider Management tools for Prowler MCP Server.
This module provides tools for managing provider connections,
including searching, connecting, and deleting providers.
@@ -19,9 +19,9 @@ class ProvidersTools(BaseTool):
"""Tools for provider management operations
Provides tools for:
- - prowler_app_search_providers: Search and view configured providers with their connection status
- - prowler_app_connect_provider: Connect or register a provider for security scanning in Prowler
- - prowler_app_delete_provider: Permanently remove a provider from Prowler
+ - prowler_search_providers: Search and view configured providers with their connection status
+ - prowler_connect_provider: Connect or register a provider for security scanning in Prowler
+ - prowler_delete_provider: Permanently remove a provider from Prowler
"""
async def search_providers(
@@ -145,7 +145,7 @@ class ProvidersTools(BaseTool):
) -> dict[str, Any]:
"""Register a provider to be scanned with Prowler.
- This tool will register a provider in Prowler App, even if the UID is wrong.
+ This tool will register a provider in Prowler, even if the UID is wrong.
If the provider is already registered, it will be updated with the new provided alias or credentials if provided.
If credentials are provided, they will be added to the indicated provider, if the provider does not exist, it will be created and the credentials will be added to it.
If the connection test is successful, the provider will be connected.
@@ -292,13 +292,13 @@ class ProvidersTools(BaseTool):
async def delete_provider(
self,
provider_id: str = Field(
- description="Prowler's internal UUID (v4) for the provider to permanently remove, generated when the provider was registered in the system. Use `prowler_app_search_providers` tool to find the provider_id if you only know the alias or the provider's own identifier (provider_uid)"
+ description="Prowler's internal UUID (v4) for the provider to permanently remove, generated when the provider was registered in the system. Use `prowler_search_providers` tool to find the provider_id if you only know the alias or the provider's own identifier (provider_uid)"
),
) -> dict[str, Any]:
"""Permanently remove a registered provider from Prowler.
WARNING: This is a destructive operation that cannot be undone. The provider will need to be
- re-added with prowler_app_connect_provider if you want to scan it again.
+ re-added with prowler_connect_provider if you want to scan it again.
The tool always returns the deletion status and message.
"""
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py b/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py
index 011e013b91..88fcca25ae 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/resources.py
@@ -1,4 +1,4 @@
-"""Cloud Resources tools for Prowler App MCP Server.
+"""Cloud Resources tools for Prowler MCP Server.
This module provides tools for searching, viewing, and analyzing cloud resources
across all providers.
@@ -86,7 +86,7 @@ class ResourcesTools(BaseTool):
IMPORTANT: This tool returns LIGHTWEIGHT resource information. Use this for fast searching
and filtering across many resources. For complete configuration details, metadata, and finding
- relationships, use prowler_app_get_resource on specific resources of interest.
+ relationships, use prowler_get_resource on specific resources of interest.
This is the primary tool for browsing resources with rich filtering capabilities.
Returns current state by default (latest scan per provider). Specify dates to query
@@ -102,16 +102,16 @@ class ResourcesTools(BaseTool):
- With dates: queries historical resource state (2-day maximum range between date_from and date_to)
Each resource includes:
- - Core identification: id (UUID for prowler_app_get_resource), uid, name
+ - Core identification: id (UUID for prowler_get_resource), uid, name
- Location context: region, service, type
- Security context: failed_findings_count (number of active security issues)
- Tags: tags associated with the resource
Useful Workflow:
1. Use this tool to search and filter resources by provider, region, service, tags, etc.
- 2. Use prowler_app_get_resource with the resource 'id' to get complete configuration and metadata
- 3. Use prowler_app_search_security_findings to find security issues for specific resources
- 4. Use prowler_app_get_finding_details to get details about the security issues for specific resources
+ 2. Use prowler_get_resource with the resource 'id' to get complete configuration and metadata
+ 3. Use prowler_search_security_findings to find security issues for specific resources
+ 4. Use prowler_get_finding_details to get details about the security issues for specific resources
"""
# Validate page_size parameter
self.api_client.validate_page_size(page_size)
@@ -177,15 +177,15 @@ class ResourcesTools(BaseTool):
async def get_resource(
self,
resource_id: str = Field(
- description="Prowler's internal UUID (v4) for the resource to retrieve, generated when the resource was discovered in the system. Use `prowler_app_list_resources` tool to find the right ID"
+ description="Prowler's internal UUID (v4) for the resource to retrieve, generated when the resource was discovered in the system. Use `prowler_list_resources` tool to find the right ID"
),
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific resource by its ID.
IMPORTANT: This tool provides COMPLETE resource details with all available information.
- Use this after finding a specific resource via prowler_app_list_resources.
+ Use this after finding a specific resource via prowler_list_resources.
- This tool provides ALL information that prowler_app_list_resources returns PLUS:
+ This tool provides ALL information that prowler_list_resources returns PLUS:
1. Configuration Details:
- metadata: Provider-specific configuration (tags, policies, encryption settings, network rules)
@@ -197,12 +197,12 @@ class ResourcesTools(BaseTool):
3. Security Relationships:
- finding_ids: Prowler's internal UUIDs (v4) of all security findings associated with this resource
- - Use prowler_app_get_finding_details on these IDs to get remediation guidance
+ - Use prowler_get_finding_details on these IDs to get remediation guidance
Useful Workflow:
- 1. Use prowler_app_list_resources to browse and filter across many resources
+ 1. Use prowler_list_resources to browse and filter across many resources
2. Use this tool to drill down into specific resources of interest
- 3. Use prowler_app_get_finding_details to get details about the security issues for specific resources
+ 3. Use prowler_get_finding_details to get details about the security issues for specific resources
"""
params = {}
@@ -348,7 +348,7 @@ class ResourcesTools(BaseTool):
async def get_resource_events(
self,
resource_id: str = Field(
- description="Prowler's internal UUID (v4) for the resource. Use `prowler_app_list_resources` to find the right ID, or get it from a finding's resource relationship via `prowler_app_get_finding_details`."
+ description="Prowler's internal UUID (v4) for the resource. Use `prowler_list_resources` to find the right ID, or get it from a finding's resource relationship via `prowler_get_finding_details`."
),
lookback_days: int = Field(
default=90,
@@ -386,8 +386,8 @@ class ResourcesTools(BaseTool):
- Identifying unauthorized or unexpected modifications
Workflows:
- 1. Resource browsing: prowler_app_list_resources → find resource → this tool for event history
- 2. Incident investigation: prowler_app_get_finding_details → get resource ID from finding → this tool to identify who caused the issue, what they changed, and when
+ 1. Resource browsing: prowler_list_resources → find resource → this tool for event history
+ 2. Incident investigation: prowler_get_finding_details → get resource ID from finding → this tool to identify who caused the issue, what they changed, and when
"""
params = {
"lookback_days": lookback_days,
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/roles.py b/mcp_server/prowler_mcp_server/prowler_app/tools/roles.py
new file mode 100644
index 0000000000..113694d8e8
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/roles.py
@@ -0,0 +1,222 @@
+"""Role (RBAC) tools for Prowler MCP Server.
+
+This module provides read tools for browsing roles and inspecting the role a
+user holds, plus a tool for setting it.
+
+A user holds exactly one role: the API resolves a user's permissions from a
+single role (`get_role` in `api.rbac.permissions`) and the UI only ever assigns
+one, so setting a role replaces the one the user currently holds instead of
+adding to it.
+"""
+
+from typing import Any
+
+from pydantic import Field
+
+from prowler_mcp_server.prowler_app.models.roles import (
+ DetailedRole,
+ RolesListResponse,
+ UserRolesResult,
+)
+from prowler_mcp_server.prowler_app.tools.base import BaseTool
+
+
+class RolesTools(BaseTool):
+ """Tools for RBAC role operations.
+
+ Provides tools for:
+ - prowler_list_roles: List the roles defined in the tenant
+ - prowler_get_role: Get detailed information about a specific role by ID
+ - prowler_get_user_roles: List the roles assigned to a specific user
+ - prowler_set_user_role: Set the role a user holds (idempotent)
+ """
+
+ async def list_roles(
+ self,
+ page_size: int = Field(
+ default=50, description="Number of results to return per page"
+ ),
+ page_number: int = Field(
+ default=1, description="Page number to retrieve (1-indexed)"
+ ),
+ ) -> dict[str, Any]:
+ """List the RBAC roles defined in the authenticated tenant.
+
+ Use this to discover which roles exist and their permission scope before
+ assigning one to a user. Returns LIGHTWEIGHT role information.
+
+ Each role includes:
+ - id: Prowler internal UUID (v4), used with `prowler_get_role` and the assignment tools
+ - name: Human-readable role name
+ - permission_state: Summary of what the role grants ('unlimited', 'limited' or 'none')
+
+ For the concrete capabilities a role grants and the users/provider groups
+ it relates to, use `prowler_get_role`.
+ """
+ self.api_client.validate_page_size(page_size)
+
+ params: dict[str, Any] = {
+ "fields[roles]": "name,permission_state",
+ "page[number]": page_number,
+ "page[size]": page_size,
+ }
+
+ clean_params = self.api_client.build_filter_params(params)
+
+ api_response = await self.api_client.get("/roles", params=clean_params)
+ simplified_response = RolesListResponse.from_api_response(api_response)
+
+ return simplified_response.model_dump()
+
+ async def get_role(
+ self,
+ role_id: str = Field(
+ description="Prowler's internal UUID (v4) for the role to retrieve. Use `prowler_list_roles` to find role IDs if you only know a name."
+ ),
+ ) -> dict[str, Any]:
+ """Retrieve detailed information about a specific role by its ID.
+
+ Returns everything `prowler_list_roles` returns PLUS:
+ - permissions: The management capabilities the role grants (only the enabled ones). Read `permission_state` for the authoritative summary: 'unlimited' means the role grants every capability, including any this deployment does not list individually
+
+ - unlimited_visibility: Whether the role can see all providers or only its provider groups
+ - provider_group_ids: Provider groups the role is scoped to (empty list means it is scoped to no provider group)
+ - user_ids: Users the role is assigned to (empty list means it is assigned to no user)
+ - inserted_at / updated_at: Lifecycle timestamps
+
+ The `user_ids` and `provider_group_ids` fields are always present: an
+ empty list means "none", not "unknown".
+
+ Workflow:
+ 1. Use `prowler_list_roles` to browse roles and find the target role 'id'
+ 2. Use this tool with that 'id' to inspect exactly what the role grants
+ """
+ api_response = await self.api_client.get(f"/roles/{role_id}")
+ detailed_role = DetailedRole.from_api_response(api_response["data"])
+
+ return detailed_role.model_dump()
+
+ async def get_user_roles(
+ self,
+ user_id: str = Field(
+ description="Prowler's internal UUID (v4) for the user whose roles you want. Use `prowler_list_users` to find user IDs, or `prowler_get_current_user` for the caller."
+ ),
+ ) -> dict[str, Any]:
+ """List the roles currently assigned to a specific user.
+
+ Returns the user's roles with the concrete capabilities each one grants,
+ so you can see what the user is allowed to do in the tenant. A user
+ normally holds a single role.
+
+ Note: this reads the user's record, so it requires MANAGE_USERS (the same
+ permission `prowler_get_user` needs). Each role's `user_ids` and
+ `provider_group_ids` are not resolved here; use `prowler_get_role` for a
+ role's full assignment and provider-group scope.
+
+ Workflow:
+ 1. Use `prowler_list_users` (or `prowler_get_current_user`) to find the user 'id'
+ 2. Use this tool to see which role they hold and what it grants
+ 3. Use `prowler_set_user_role` to change it
+ """
+ roles = await self._fetch_user_roles(user_id)
+
+ return UserRolesResult.build(user_id=user_id, roles=roles).model_dump()
+
+ async def set_user_role(
+ self,
+ user_id: str = Field(
+ description="Prowler's internal UUID (v4) for the user whose role you want to set. Use `prowler_list_users` to find user IDs."
+ ),
+ role_id: str = Field(
+ description="Prowler's internal UUID (v4) for the role the user should hold. Use `prowler_list_roles` to find role IDs."
+ ),
+ ) -> dict[str, Any]:
+ """Set the role a user holds, replacing the role they had before.
+
+ A user holds exactly one role in Prowler: their permissions are resolved
+ from a single role, so granting a new one REPLACES the previous one
+ instead of adding to it. To change what a user can do, set the role that
+ grants the capabilities they should have.
+
+ This tool is idempotent: if the user already holds only this role, it
+ makes no change and reports `changed: false`. It always returns the
+ user's up-to-date role after the operation.
+
+ Note: this operation requires both MANAGE_ACCOUNT (to change role
+ assignments) and MANAGE_USERS (to read the user's current role). The API
+ rejects the change when it would leave the tenant without a user holding
+ MANAGE_ACCOUNT; such rejections are surfaced as errors.
+
+ Workflow:
+ 1. Use `prowler_list_roles` to find the role 'id' to grant
+ 2. Use `prowler_list_users` to find the target user 'id'
+ 3. Use this tool to set the user's role
+ """
+ current_roles = await self._fetch_user_roles(user_id)
+ if [role.id for role in current_roles] == [role_id]:
+ return UserRolesResult.build(
+ user_id=user_id,
+ roles=current_roles,
+ changed=False,
+ message=f"User {user_id} already holds role {role_id}; no change made.",
+ ).model_dump()
+
+ # The relationship endpoint accepts any well-formed UUID and silently
+ # drops role IDs that do not exist in this tenant, which would leave the
+ # user with no role at all. Confirm the role exists before replacing.
+ try:
+ await self.api_client.get(f"/roles/{role_id}")
+ except Exception as e:
+ raise ValueError(
+ f"Role {role_id} could not be read ({e}), so user {user_id} was left "
+ f"unchanged. Use `prowler_list_roles` to find a valid role ID."
+ ) from e
+
+ # PATCH replaces the user's whole role set with this single role, the
+ # same call the Prowler UI makes when changing a user's role.
+ await self.api_client.patch(
+ f"/users/{user_id}/relationships/roles",
+ json_data={"data": [{"type": "roles", "id": role_id}]},
+ )
+
+ # After the change, fetch the user's roles again to report the authoritative state
+ updated_roles = await self._fetch_user_roles(user_id)
+
+ return UserRolesResult.build(
+ user_id=user_id,
+ roles=updated_roles,
+ changed=True,
+ message=f"Role {role_id} set for user {user_id}.",
+ ).model_dump()
+
+ # Private helper methods
+
+ async def _fetch_user_roles(self, user_id: str) -> list[DetailedRole]:
+ """Fetch the roles currently assigned to a user.
+
+ Uses a single `GET /users/{id}?include=roles` request and reads the
+ role resources from the JSON:API `included` section. This request
+ requires MANAGE_USERS (it reads the user record).
+
+ The included role resources do not carry their `users` /
+ `provider_groups` relationships, so the returned `DetailedRole`
+ instances omit `user_ids` / `provider_group_ids` (unknown here)
+ rather than reporting them as empty. Use `get_role` for a role's full
+ assignment and provider-group scope.
+
+ Args:
+ user_id: The Prowler UUID of the user
+
+ Returns:
+ The user's roles as DetailedRole instances (empty list if none)
+ """
+ response = await self.api_client.get(
+ f"/users/{user_id}", params={"include": "roles"}
+ )
+ included = response.get("included", []) or []
+
+ return [
+ DetailedRole.from_api_response(item)
+ for item in included
+ if item.get("type") == "roles"
+ ]
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/scans.py b/mcp_server/prowler_mcp_server/prowler_app/tools/scans.py
index 1df636ffc0..21d1431b71 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/tools/scans.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/scans.py
@@ -1,4 +1,4 @@
-"""Security Scans tools for Prowler App MCP Server.
+"""Security Scans tools for Prowler MCP Server.
This module provides tools for managing and monitoring Prowler security scans.
"""
@@ -20,18 +20,18 @@ class ScansTools(BaseTool):
"""Tools for security scan operations.
Provides tools for:
- - prowler_app_list_scans: Search and filter scans with rich filtering capabilities
- - prowler_app_get_scan: Get comprehensive details about a specific scan
- - prowler_app_trigger_scan: Trigger manual security scans for providers
- - prowler_app_schedule_daily_scan: Schedule automated daily scans for continuous monitoring
- - prowler_app_update_scan: Update scan names for better organization
+ - prowler_list_scans: Search and filter scans with rich filtering capabilities
+ - prowler_get_scan: Get comprehensive details about a specific scan
+ - prowler_trigger_scan: Trigger manual security scans for providers
+ - prowler_schedule_daily_scan: Schedule automated daily scans for continuous monitoring
+ - prowler_update_scan: Update scan names for better organization
"""
async def list_scans(
self,
provider_id: list[str] = Field(
default=[],
- description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s), generated when the provider was registered. Use `prowler_app_search_providers` tool to find provider IDs",
+ description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s), generated when the provider was registered. Use `prowler_search_providers` tool to find provider IDs",
),
provider_type: list[str] = Field(
default=[],
@@ -56,7 +56,7 @@ class ScansTools(BaseTool):
),
trigger: Literal["manual", "scheduled"] | None = Field(
default=None,
- description="Filter by how the scan was initiated. Options: 'manual' (user-initiated via prowler_app_trigger_scan), 'scheduled' (automated via prowler_app_schedule_daily_scan)",
+ description="Filter by how the scan was initiated. Options: 'manual' (user-initiated via prowler_trigger_scan), 'scheduled' (automated via prowler_schedule_daily_scan)",
),
name: str | None = Field(
default=None,
@@ -75,7 +75,7 @@ class ScansTools(BaseTool):
IMPORTANT: This tool returns LIGHTWEIGHT scan information. Use this for fast searching and filtering
across many scans. For complete scan details including progress, duration, and resource counts,
- use prowler_app_get_scan on specific scans of interest.
+ use prowler_get_scan on specific scans of interest.
Default behavior:
- Returns all scans
@@ -83,15 +83,15 @@ class ScansTools(BaseTool):
- Includes all scan states (available, scheduled, executing, completed, failed, cancelled)
Each scan includes:
- - Core identification: id (UUID for prowler_app_get_scan), name
+ - Core identification: id (UUID for prowler_get_scan), name
- Execution context: state, trigger (manual/scheduled)
- Temporal data: started_at, completed_at
- Provider relationship: provider_id
Workflow:
1. Use this tool to search and filter scans by provider, state, or date range
- 2. Use prowler_app_get_scan with the scan 'id' to get progress, duration, and resource counts
- 3. Use prowler_app_search_security_findings filtered by scan dates to analyze scan results
+ 2. Use prowler_get_scan with the scan 'id' to get progress, duration, and resource counts
+ 3. Use prowler_search_security_findings filtered by scan dates to analyze scan results
"""
# Validate pagination
self.api_client.validate_page_size(page_size)
@@ -128,15 +128,15 @@ class ScansTools(BaseTool):
async def get_scan(
self,
scan_id: str = Field(
- description="Prowler's internal UUID (v4) for the scan to retrieve, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find scan IDs"
+ description="Prowler's internal UUID (v4) for the scan to retrieve, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_list_scans` tool to find scan IDs"
),
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific scan by its ID.
IMPORTANT: This tool returns COMPLETE scan details.
- Use this after finding a specific scan via prowler_app_list_scans.
+ Use this after finding a specific scan via prowler_list_scans.
- This tool provides ALL information that prowler_app_list_scans returns PLUS:
+ This tool provides ALL information that prowler_list_scans returns PLUS:
1. Execution Details:
- progress: Scan completion progress as percentage (0-100%)
@@ -155,9 +155,9 @@ class ScansTools(BaseTool):
- Understanding scan scheduling patterns
Workflow:
- 1. Use prowler_app_list_scans to browse and filter scans
+ 1. Use prowler_list_scans to browse and filter scans
2. Use this tool with the scan 'id' to monitor progress or view detailed results
- 3. For completed scans, use prowler_app_search_security_findings filtered by date to analyze findings
+ 3. For completed scans, use prowler_search_security_findings filtered by date to analyze findings
"""
# Fetch scan with all fields
params = {
@@ -172,7 +172,7 @@ class ScansTools(BaseTool):
async def trigger_scan(
self,
provider_id: str = Field(
- description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
+ description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_search_providers` tool to find the provider ID"
),
name: str | None = Field(
default=None,
@@ -182,14 +182,14 @@ class ScansTools(BaseTool):
"""Trigger a manual security scan for a provider.
IMPORTANT: This tool returns immediately once the scan is created.
- The scan will continue running in the background. Use `prowler_app_get_scan`
+ The scan will continue running in the background. Use `prowler_get_scan`
with the returned scan ID to monitor progress and check when it completes.
Example Useful Workflow:
- 1. Use `prowler_app_search_providers` to find the provider_id you want to scan
+ 1. Use `prowler_search_providers` to find the provider_id you want to scan
2. Use this tool to trigger the scan
- 3. Use `prowler_app_get_scan` with the returned scan 'id' to monitor progress
- 4. Once completed, use `prowler_app_search_security_findings` to analyze results
+ 3. Use `prowler_get_scan` with the returned scan 'id' to monitor progress
+ 4. Once completed, use `prowler_search_security_findings` to analyze results
"""
try:
# Build request data
@@ -231,7 +231,7 @@ class ScansTools(BaseTool):
return ScanCreationResult(
scan=scan_info,
status="success",
- message=f"Scan {scan_id} created successfully. The scan may take some time to complete. Use prowler_app_get_scan tool with this ID to monitor progress.",
+ message=f"Scan {scan_id} created successfully. The scan may take some time to complete. Use prowler_get_scan tool with this ID to monitor progress.",
).model_dump()
except Exception as e:
@@ -245,7 +245,7 @@ class ScansTools(BaseTool):
async def schedule_daily_scan(
self,
provider_id: str = Field(
- description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
+ description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_search_providers` tool to find the provider ID"
),
) -> dict[str, Any]:
"""Schedule automated daily scans for a provider for continuous security monitoring.
@@ -256,17 +256,17 @@ class ScansTools(BaseTool):
you're not actively using the system.
IMPORTANT: This tool returns immediately once the daily schedule is created.
- The schedule will be set up in the background. Use `prowler_app_list_scans`
+ The schedule will be set up in the background. Use `prowler_list_scans`
filtered by provider_id and trigger='scheduled' to view scheduled scans.
IMPORTANT: This creates a PERSISTENT schedule. The provider will be scanned
automatically every 24 hours until the provider is deleted.
Example Useful Workflow:
- 1. Use `prowler_app_search_providers` to find the provider_id you want to monitor
+ 1. Use `prowler_search_providers` to find the provider_id you want to monitor
2. Use this tool to create the daily schedule
- 3. Use `prowler_app_list_scans` filtered by provider_id to view scheduled and completed scans
- 4. Monitor findings over time with `prowler_app_search_security_findings`
+ 3. Use `prowler_list_scans` filtered by provider_id to view scheduled and completed scans
+ 4. Monitor findings over time with `prowler_search_security_findings`
"""
self.logger.info(f"Creating daily schedule for provider {provider_id}")
task_response = await self.api_client.post(
@@ -285,7 +285,7 @@ class ScansTools(BaseTool):
)
if task_state == "available":
- return_message = "Daily schedule created successfully. The schedule is being set up in the background. Use prowler_app_list_scans with provider_id filter to view scheduled scans."
+ return_message = "Daily schedule created successfully. The schedule is being set up in the background. Use prowler_list_scans with provider_id filter to view scheduled scans."
else:
return_message = "Daily schedule creation failed. Please try again later."
@@ -297,7 +297,7 @@ class ScansTools(BaseTool):
async def update_scan(
self,
scan_id: str = Field(
- description="Prowler's internal UUID (v4) for the scan to update, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find the scan ID if you only know the provider or scan name. Returns an error if the scan ID is invalid or not found."
+ description="Prowler's internal UUID (v4) for the scan to update, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_list_scans` tool to find the scan ID if you only know the provider or scan name. Returns an error if the scan ID is invalid or not found."
),
name: str = Field(
description="New human-friendly name for the scan (3-100 characters). Use descriptive names to improve organization and tracking, e.g., 'Production Security Audit - Q4 2025', 'Post-Deployment Compliance Check'. IMPORTANT: Only the scan name can be updated - other attributes (state, progress, duration) are read-only and managed by the system."
@@ -309,7 +309,7 @@ class ScansTools(BaseTool):
(state, progress, duration, etc.) are read-only and managed by the system.
Example Useful Workflow:
- 1. Use `prowler_app_list_scans` to find the scan you want to rename
+ 1. Use `prowler_list_scans` to find the scan you want to rename
2. Use this tool with the scan 'id' and new name
"""
api_response = await self.api_client.patch(
diff --git a/mcp_server/prowler_mcp_server/prowler_app/tools/users.py b/mcp_server/prowler_mcp_server/prowler_app/tools/users.py
new file mode 100644
index 0000000000..a7e31b60ad
--- /dev/null
+++ b/mcp_server/prowler_mcp_server/prowler_app/tools/users.py
@@ -0,0 +1,121 @@
+"""User management tools for Prowler MCP Server.
+
+This module provides read-only tools for viewing the users that belong to the
+authenticated tenant, including identifying which user the current credentials
+(API key or JWT) authenticate as.
+"""
+
+from typing import Any
+
+from pydantic import Field
+
+from prowler_mcp_server.prowler_app.models.users import (
+ DetailedUser,
+ UsersListResponse,
+)
+from prowler_mcp_server.prowler_app.tools.base import BaseTool
+
+
+class UsersTools(BaseTool):
+ """Tools for user management operations (read-only).
+
+ Provides tools for:
+ - prowler_list_users: List the users in the tenant with their names and emails
+ - prowler_get_user: Get detailed information about a specific user by ID
+ - prowler_get_current_user: Identify which user the current credentials authenticate as
+ """
+
+ async def list_users(
+ self,
+ name: str | None = Field(
+ default=None,
+ description="Filter by user display name. Partial match supported (case-insensitive).",
+ ),
+ email: str | None = Field(
+ default=None,
+ description="Filter by user email address. Partial match supported (case-insensitive).",
+ ),
+ page_size: int = Field(
+ default=50, description="Number of results to return per page"
+ ),
+ page_number: int = Field(
+ default=1, description="Page number to retrieve (1-indexed)"
+ ),
+ ) -> dict[str, Any]:
+ """List the users that belong to the authenticated tenant.
+
+ Use this to see who has access to the tenant and to look up their email
+ addresses. Returns LIGHTWEIGHT user information optimized for browsing.
+
+ Each user includes:
+ - id: Prowler internal UUID (v4), used with `prowler_get_user`
+ - name: Display name
+ - email: Email address
+ - company_name: Company the user belongs to, when set
+
+ To find out which user the current credentials authenticate as, use
+ `prowler_get_current_user`. For a single user's roles, membership links
+ and join date, use `prowler_get_user`.
+ """
+ self.api_client.validate_page_size(page_size)
+
+ params: dict[str, Any] = {
+ "fields[users]": "name,email,company_name",
+ "page[number]": page_number,
+ "page[size]": page_size,
+ }
+
+ if name:
+ params["filter[name__icontains]"] = name
+ if email:
+ params["filter[email__icontains]"] = email
+
+ clean_params = self.api_client.build_filter_params(params)
+
+ api_response = await self.api_client.get("/users", params=clean_params)
+ simplified_response = UsersListResponse.from_api_response(api_response)
+
+ return simplified_response.model_dump()
+
+ async def get_user(
+ self,
+ user_id: str = Field(
+ description="Prowler's internal UUID (v4) for the user to retrieve. Use `prowler_list_users` to find user IDs if you only know a name or email."
+ ),
+ ) -> dict[str, Any]:
+ """Retrieve detailed information about a specific user by their ID.
+
+ Returns everything `prowler_list_users` returns PLUS:
+ - date_joined: When the user joined
+ - role_ids: UUIDs of the roles assigned to the user
+ - membership_ids: UUIDs of the user's tenant memberships
+
+ Reading another user's roles/memberships requires MANAGE_ACCOUNT; without
+ it the API hides them and `role_ids`/`membership_ids` are omitted rather
+ than reported as empty.
+
+ Workflow:
+ 1. Use `prowler_list_users` to browse users and find the target user 'id'
+ 2. Use this tool with that 'id' to inspect the user's roles and account details
+ """
+ api_response = await self.api_client.get(f"/users/{user_id}")
+ detailed_user = DetailedUser.from_api_response(api_response["data"])
+
+ return detailed_user.model_dump()
+
+ async def get_current_user(self) -> dict[str, Any]:
+ """Identify which user the current credentials authenticate as.
+
+ Use this to determine the identity behind the credentials this MCP server
+ is currently using, e.g. before performing actions on behalf of that user
+ or when reporting who is connected.
+
+ Returns the same detailed information as `prowler_get_user`:
+ - id, name, email, company_name
+ - date_joined
+ - role_ids, membership_ids
+ """
+ api_response = await self.api_client.get("/users/me")
+ detailed_user = DetailedUser.from_api_response(api_response["data"])
+
+ return detailed_user.model_dump()
diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py
index a6aacc3ce1..5717a0a3b5 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/utils/api_client.py
@@ -1,4 +1,4 @@
-"""Shared API client utilities for Prowler App tools."""
+"""Shared API client utilities for Prowler tools."""
import asyncio
from datetime import datetime, timedelta
@@ -15,6 +15,20 @@ from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth
ALLOWED_EXTERNAL_DOMAINS: frozenset[str] = frozenset({"raw.githubusercontent.com"})
+class ProwlerAPIError(Exception):
+ """An error response returned by the Prowler API.
+
+ Raised only when the API answered with an error status, which tells a caller
+ something no plain exception can: the request reached Prowler and was
+ rejected, so it changed nothing. A timeout or a dropped connection stays a
+ bare exception because the request may well have been processed.
+ """
+
+ def __init__(self, message: str, status_code: int) -> None:
+ super().__init__(message)
+ self.status_code: int = status_code
+
+
class HTTPMethod(StrEnum):
"""HTTP methods enum."""
@@ -73,7 +87,8 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
API response as dictionary
Raises:
- Exception: If API request fails
+ ProwlerAPIError: If the API answered with an error status
+ Exception: If the request could not be completed
"""
try:
token: str = await self.auth_manager.get_valid_token()
@@ -105,8 +120,9 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
except Exception:
error_detail = e.response.text
- raise Exception(
- f"API request failed: {e.response.status_code} - {error_detail}"
+ raise ProwlerAPIError(
+ f"API request failed: {e.response.status_code} - {error_detail}",
+ e.response.status_code,
)
except Exception as e:
logger.error(f"Error during {method.value} {path}: {e}")
@@ -176,13 +192,19 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
)
async def delete(
- self, path: str, params: dict[str, any] | None = None
+ self,
+ path: str,
+ params: dict[str, any] | None = None,
+ json_data: dict[str, any] | None = None,
) -> dict[str, any]:
"""Make DELETE request.
Args:
path: API endpoint path
params: Optional query parameters
+ json_data: Optional JSON body data. Some JSON:API relationship
+ endpoints (e.g. ``/users/{id}/relationships/roles``) accept a
+ body listing the specific members to remove.
Returns:
API response as dictionary
@@ -190,7 +212,9 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
Raises:
Exception: If API request fails
"""
- return await self._make_request(HTTPMethod.DELETE, path, params=params)
+ return await self._make_request(
+ HTTPMethod.DELETE, path, params=params, json_data=json_data
+ )
async def fetch_external_url(self, url: str) -> str:
"""Fetch content from an allowed external URL (unauthenticated).
diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py
index 72c06000df..eff5d3a117 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/utils/auth.py
@@ -10,7 +10,7 @@ from prowler_mcp_server.lib.logger import logger
class ProwlerAppAuth:
- """Handles authentication for Prowler App API using API keys or JWT tokens."""
+ """Handles authentication for Prowler API using API keys or JWT tokens."""
def __init__(
self,
@@ -18,19 +18,23 @@ class ProwlerAppAuth:
base_url: str = os.getenv("API_BASE_URL", "https://api.prowler.com/api/v1"),
):
self.base_url = base_url.rstrip("/")
- logger.info(f"Using Prowler App API base URL: {self.base_url}")
+ logger.info(f"Using Prowler API base URL: {self.base_url}")
self.mode = mode
self.access_token: str | None = None
self.api_key: str | None = None
if mode == "stdio": # STDIO mode
- self.api_key = os.getenv("PROWLER_APP_API_KEY")
+ # PROWLER_API_KEY is the current variable; PROWLER_APP_API_KEY is kept
+ # as a backward-compatible fallback so existing setups keep working.
+ self.api_key = os.getenv("PROWLER_API_KEY") or os.getenv(
+ "PROWLER_APP_API_KEY"
+ )
if not self.api_key:
- raise ValueError("PROWLER_APP_API_KEY environment variable is required")
+ raise ValueError("PROWLER_API_KEY environment variable is required")
if not self.api_key.startswith("pk_"):
- raise ValueError("Prowler App API key format is incorrect")
+ raise ValueError("Prowler API key format is incorrect")
def _parse_jwt(self, token: str) -> dict | None:
"""Parse JWT token and return payload
diff --git a/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py b/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py
index b85c13af35..3a00474b5f 100644
--- a/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py
+++ b/mcp_server/prowler_mcp_server/prowler_app/utils/tool_loader.py
@@ -13,18 +13,27 @@ from prowler_mcp_server.lib.logger import logger
from prowler_mcp_server.prowler_app.tools.base import BaseTool
-def load_all_tools(mcp: FastMCP) -> None:
- """Auto-discover and load all BaseTool subclasses from the tools package.
+def load_all_tools(
+ mcp: FastMCP,
+ tools_package: str = "prowler_mcp_server.prowler_app.tools",
+) -> None:
+ """Auto-discover and load all BaseTool subclasses from a tools package.
This function:
- 1. Dynamically imports all Python modules in the tools package
- 2. Discovers all concrete BaseTool subclasses
+ 1. Dynamically imports all Python modules in the given tools package
+ 2. Discovers all concrete BaseTool subclasses defined in that package
3. Instantiates each tool class
4. Registers all tools with the provided FastMCP instance
+ ``BaseTool.__subclasses__()`` returns every subclass in the process, so the
+ discovered classes are filtered by ``__module__`` prefix. This keeps sibling
+ sub-servers (e.g. ``prowler_app`` and ``prowler_cloud``) from cross-registering
+ each other's tools, regardless of import order.
+
Args:
mcp: The FastMCP instance to register tools with
- TOOLS_PACKAGE: The package path containing tool modules (default: prowler_mcp_server.prowler_app.tools)
+ tools_package: The package path containing tool modules
+ (default: prowler_mcp_server.prowler_app.tools)
Example:
from fastmcp import FastMCP
@@ -33,7 +42,7 @@ def load_all_tools(mcp: FastMCP) -> None:
app = FastMCP("prowler-app")
load_all_tools(app)
"""
- TOOLS_PACKAGE = "prowler_mcp_server.prowler_app.tools"
+ TOOLS_PACKAGE = tools_package
logger.info(f"Auto-discovering tools from package: {TOOLS_PACKAGE}")
# Import the tools package
@@ -59,11 +68,14 @@ def load_all_tools(mcp: FastMCP) -> None:
except Exception as e:
logger.error(f"Failed to import module {module_name}: {e}")
- # Discover all concrete BaseTool subclasses
+ # Discover all concrete BaseTool subclasses defined in this package only.
+ # __subclasses__() is process-wide, so filter by module to avoid sibling
+ # sub-servers cross-registering each other's tools.
concrete_tools = [
tool_class
for tool_class in BaseTool.__subclasses__()
if not getattr(tool_class, "__abstractmethods__", None)
+ and tool_class.__module__.startswith(TOOLS_PACKAGE)
]
logger.info(f"Discovered {len(concrete_tools)} tool classes")
diff --git a/mcp_server/prowler_mcp_server/server.py b/mcp_server/prowler_mcp_server/server.py
index 7c85641dee..1b3271be38 100644
--- a/mcp_server/prowler_mcp_server/server.py
+++ b/mcp_server/prowler_mcp_server/server.py
@@ -19,15 +19,15 @@ def setup_main_server():
except Exception as e:
logger.error(f"Failed to mount Prowler Hub server: {e}")
- # Mount Prowler App tools with prowler_app_ namespace
+ # Mount core Prowler tools with prowler_ namespace
try:
- logger.info("Mounting Prowler App server...")
+ logger.info("Mounting Prowler tools server...")
from prowler_mcp_server.prowler_app.server import app_mcp_server
- prowler_mcp_server.mount(app_mcp_server, namespace="prowler_app")
- logger.info("Successfully mounted Prowler App server")
+ prowler_mcp_server.mount(app_mcp_server, namespace="prowler")
+ logger.info("Successfully mounted Prowler tools server")
except Exception as e:
- logger.error(f"Failed to mount Prowler App server: {e}")
+ logger.error(f"Failed to mount Prowler tools server: {e}")
# Mount Prowler Documentation tools with prowler_docs_ namespace
try:
@@ -61,4 +61,5 @@ async def health_check(_request) -> JSONResponse:
setup_main_server()
-app = prowler_mcp_server.http_app()
+# ASGI app for uvicorn deployments; stateless to avoid retaining sessions
+app = prowler_mcp_server.http_app(stateless_http=True)
diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml
index 63aefdf931..30016bf5cb 100644
--- a/mcp_server/pyproject.toml
+++ b/mcp_server/pyproject.toml
@@ -5,28 +5,58 @@ requires = ["setuptools>=61.0", "wheel"]
[dependency-groups]
dev = [
"bandit==1.8.3",
+ "coverage==7.15.2",
"pytest==9.0.3",
+ "pytest-asyncio==1.4.0",
+ "pytest-cov==6.0.0",
+ "pytest-env==1.1.5",
"ruff==0.15.11",
"vulture==2.14"
]
[project]
dependencies = [
- "fastmcp==3.2.4",
+ "fastmcp==3.4.5",
"httpx==0.28.1"
]
description = "MCP server for Prowler ecosystem"
name = "prowler-mcp"
readme = "README.md"
requires-python = ">=3.12"
-version = "0.5.0"
+version = "0.9.0"
[project.scripts]
prowler-mcp = "prowler_mcp_server.main:main"
+[tool.pytest]
+
[tool.pytest.ini_options]
+addopts = "--strict-markers --strict-config"
+# `asyncio_mode = "auto"` lets `async def test_*` run without a per-test marker;
+# the server is async end to end, so requiring one would be pure noise. Setting
+# the fixture loop scope explicitly silences a pytest-asyncio deprecation warning.
+asyncio_default_fixture_loop_scope = "function"
+asyncio_mode = "auto"
+filterwarnings = [
+ "error",
+ # Starlette's TestClient warns that it will require httpx2. The httpx pin is a
+ # deliberate project-wide choice, so this stays allowed until that pin moves.
+ "default::starlette.exceptions.StarletteDeprecationWarning"
+]
+pythonpath = ["."]
testpaths = ["tests"]
+# Applied before any conftest or test module is imported, which is what makes it
+# work: `prowler_app/server.py` builds every tool at import time, and a tool whose
+# construction raises (as it does without an API key) is swallowed by
+# `load_all_tools`, leaving the `prowler_*` namespace silently empty. Pinning a
+# fake key here keeps the full tool surface loadable and stops a developer's
+# `mcp_server/.env` from reaching the suite.
+[tool.pytest_env]
+API_BASE_URL = "https://api.testing.invalid/api/v1"
+PROWLER_API_KEY = "pk_fake_api_key_for_unit_testing_only"
+PROWLER_MCP_TRANSPORT_MODE = "stdio"
+
# Shared ruff baseline (kept in sync with api/pyproject.toml).
# target-version tracks this project's lowest supported Python.
[tool.ruff]
@@ -45,3 +75,11 @@ extend-select = [
[tool.uv]
package = true
+
+# Transitive pins fastmcp does not raise on its own; each carries a known HIGH.
+constraint-dependencies = [
+ "cryptography==50.0.0",
+ "joserfc==1.6.8",
+ "mcp==1.28.1",
+ "python-multipart==0.0.30"
+]
diff --git a/mcp_server/tests/conftest.py b/mcp_server/tests/conftest.py
new file mode 100644
index 0000000000..6e1fb672ba
--- /dev/null
+++ b/mcp_server/tests/conftest.py
@@ -0,0 +1,264 @@
+"""Shared fixtures for the Prowler MCP Server test suite.
+
+This module deliberately does not import ``prowler_mcp_server.server`` at module
+scope. That import builds every tool and reads the environment, so it must happen
+only once the environment is settled. Environment pinning itself lives in
+``[tool.pytest_env]`` in ``pyproject.toml``, which is applied before any conftest
+or test module is imported; the fixtures here only keep it pinned per test.
+
+Three properties of the runtime shape everything below and are easy to get wrong:
+
+1. ``prowler_app/server.py`` builds every tool at import time. A tool whose
+ construction raises -- which is what happens with no API key -- is swallowed by
+ ``load_all_tools``, leaving the ``prowler_*`` namespace silently empty. So the
+ suite pins a fake key rather than stripping the real one.
+2. ``BaseTool.__init__`` captured the ``ProwlerAPIClient`` singleton by reference
+ at import time. Evicting it from the registry does not re-point the tools, so
+ the client must be patched in place.
+3. ``ProwlerAppAuth`` resolves ``PROWLER_MCP_TRANSPORT_MODE`` and ``API_BASE_URL``
+ in its default arguments, which are evaluated once at module import.
+ ``monkeypatch.setenv`` cannot change them -- pass ``mode=``/``base_url=``
+ explicitly instead.
+"""
+
+import socket
+from collections.abc import Callable, Iterator
+
+import httpx
+import pytest
+from starlette.requests import Request
+from starlette.testclient import TestClient
+
+from tests.helpers.http import MockRouter
+from tests.helpers.tokens import FAKE_API_KEY
+
+# Must match [tool.pytest_env] in pyproject.toml: the env var is what the code
+# reads at import time, this constant is what tests assert against.
+TEST_API_BASE_URL = "https://api.testing.invalid/api/v1"
+
+
+# --------------------------------------------------------------- environment
+
+
+@pytest.fixture(autouse=True)
+def _pinned_environment(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Pin the runtime environment to deterministic test values.
+
+ Pinned rather than stripped: a missing ``PROWLER_API_KEY`` collapses the
+ ``prowler_*`` namespace to zero tools instead of failing loudly.
+ ``PROWLER_APP_API_KEY`` is the deprecated fallback and is removed so only a
+ test that sets it exercises that path.
+
+ This also stops a developer's gitignored ``mcp_server/.env`` or shell
+ environment from reaching the suite.
+ """
+ monkeypatch.setenv("PROWLER_API_KEY", FAKE_API_KEY)
+ monkeypatch.setenv("API_BASE_URL", TEST_API_BASE_URL)
+ monkeypatch.setenv("PROWLER_MCP_TRANSPORT_MODE", "stdio")
+ monkeypatch.delenv("PROWLER_APP_API_KEY", raising=False)
+
+
+@pytest.fixture(autouse=True)
+def _no_real_network(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Fail loudly on any real outbound socket connection.
+
+ The subject under test is an HTTP client, so a route that was not mocked must
+ fail fast and obviously rather than quietly reaching hub.prowler.com and
+ making the suite slow, flaky and dependent on someone else's uptime.
+
+ In-process transports (Starlette's ``TestClient``, fastmcp's in-memory
+ client) do not open sockets, so this does not interfere with them.
+ """
+
+ def _blocked(self: socket.socket, address: object, *_: object) -> None:
+ raise RuntimeError(
+ f"Blocked a real network connection to {address}. Drive HTTP through "
+ "the mock_api_client, hub_router or docs_router fixtures."
+ )
+
+ monkeypatch.setattr(socket.socket, "connect", _blocked)
+ monkeypatch.setattr(socket.socket, "connect_ex", _blocked)
+
+
+# ----------------------------------------------------------------- API client
+
+
+@pytest.fixture(autouse=True)
+def _singleton_registry_guard() -> Iterator[None]:
+ """Snapshot and restore the singleton registry around every test.
+
+ Deliberately a snapshot, not a clear. ``BaseTool.__init__`` captured the
+ ``ProwlerAPIClient`` instance by reference at import time, so evicting it
+ would leave every registered tool pointing at an orphan that later fixtures
+ cannot patch -- one holding a real ``httpx.AsyncClient``. Restoring keeps a
+ test that resets on purpose from leaking into the next one.
+ """
+ from prowler_mcp_server.prowler_app.utils.api_client import SingletonMeta
+
+ snapshot = dict(SingletonMeta._instances)
+ try:
+ yield
+ finally:
+ SingletonMeta._instances.clear()
+ SingletonMeta._instances.update(snapshot)
+
+
+@pytest.fixture
+def mock_router() -> MockRouter:
+ """An empty route registry and request recorder for this test."""
+ return MockRouter()
+
+
+@pytest.fixture
+def api_client():
+ """The live ``ProwlerAPIClient`` singleton that every registered tool holds."""
+ from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIClient
+
+ return ProwlerAPIClient()
+
+
+@pytest.fixture
+def mock_api_client(api_client, mock_router: MockRouter) -> Iterator:
+ """The API client singleton, with its transport driven by ``mock_router``.
+
+ Swaps ``.client`` in place rather than constructing a fresh client, so tools
+ reached through the MCP protocol -- which hold this exact instance -- are
+ mocked too. Everything else still runs for real: URL joining, query encoding,
+ auth headers, ``raise_for_status()`` and the JSON:API error unwrapping.
+ """
+ original = api_client.client
+ api_client.client = httpx.AsyncClient(transport=mock_router.transport, timeout=30.0)
+ try:
+ yield api_client
+ finally:
+ api_client.client = original
+
+
+@pytest.fixture
+def isolated_api_client() -> Iterator[type]:
+ """Evict the singleton so a test can exercise construction semantics.
+
+ Only for tests *about* ``ProwlerAPIClient`` itself -- its ``__init__`` or its
+ singleton identity. Anything reached through a tool must use
+ ``mock_api_client``, because the tools still point at the original instance.
+ """
+ from prowler_mcp_server.prowler_app.utils.api_client import (
+ ProwlerAPIClient,
+ SingletonMeta,
+ )
+
+ SingletonMeta._instances.pop(ProwlerAPIClient, None)
+ yield ProwlerAPIClient
+
+
+# --------------------------------------------------------------- MCP surface
+
+
+@pytest.fixture(scope="session")
+def mcp_root_server():
+ """The mounted root MCP server, imported lazily because importing has effects.
+
+ Tests open their own client over this (``async with Client(mcp_root_server)``)
+ rather than receiving a connected one, because FastMCP warns that holding a
+ client in a fixture causes hard-to-diagnose event-loop problems.
+ """
+ from prowler_mcp_server.server import prowler_mcp_server
+
+ return prowler_mcp_server
+
+
+@pytest.fixture
+def health_client() -> Iterator[TestClient]:
+ """An ASGI client over the stateless HTTP app, for the ``/health`` route."""
+ from prowler_mcp_server.server import app
+
+ with TestClient(app) as client:
+ yield client
+
+
+@pytest.fixture
+def http_request_headers() -> Iterator[Callable[..., None]]:
+ """Return a callable that makes ``get_http_headers()`` observe given headers.
+
+ In HTTP transport mode ``ProwlerAppAuth`` reads the authorization header
+ through fastmcp's request context variable. Setting that variable directly is
+ what lets an auth test run without standing up a real HTTP server.
+
+ Underscores in keyword names become hyphens, so ``x_request_id=`` sets
+ ``x-request-id``.
+ """
+ from fastmcp.server.http import _current_http_request
+
+ def _set(**headers: str) -> None:
+ scope = {
+ "type": "http",
+ "http_version": "1.1",
+ "method": "POST",
+ "path": "/mcp",
+ "raw_path": b"/mcp",
+ "root_path": "",
+ "scheme": "http",
+ "query_string": b"",
+ "server": ("testserver", 80),
+ "client": ("testclient", 50000),
+ "headers": [
+ (name.lower().replace("_", "-").encode(), value.encode())
+ for name, value in headers.items()
+ ],
+ }
+ _current_http_request.set(Request(scope))
+
+ try:
+ yield _set
+ finally:
+ # Not a token-based reset: an async test calls `_set` inside its task,
+ # and asyncio gives each task its own copy of the context, so the token
+ # cannot be reset from here and the task's value is discarded with the
+ # task anyway. Clearing the value covers the sync-test case, where the
+ # set would otherwise persist into the next test.
+ _current_http_request.set(None)
+
+
+# ------------------------------------------------------- hub / docs sub-servers
+
+
+def _clone_with_transport(
+ client: httpx.Client, transport: httpx.MockTransport
+) -> httpx.Client:
+ """Copy a sync client's base URL and headers onto a mock transport."""
+ return httpx.Client(
+ base_url=client.base_url,
+ headers=dict(client.headers),
+ transport=transport,
+ )
+
+
+@pytest.fixture
+def hub_router(monkeypatch: pytest.MonkeyPatch, mock_router: MockRouter) -> MockRouter:
+ """Route the Prowler Hub sub-server's two module-level sync clients.
+
+ Hub tools are synchronous and reach for these clients by module global, so
+ they are replaced on the module rather than injected.
+ """
+ from prowler_mcp_server.prowler_hub import server as hub
+
+ for name in ("prowler_hub_client", "github_raw_client"):
+ monkeypatch.setattr(
+ hub, name, _clone_with_transport(getattr(hub, name), mock_router.transport)
+ )
+ return mock_router
+
+
+@pytest.fixture
+def docs_router(monkeypatch: pytest.MonkeyPatch, mock_router: MockRouter) -> MockRouter:
+ """Route the documentation search engine's two sync clients."""
+ from prowler_mcp_server.prowler_documentation import server as docs
+
+ engine = docs.prowler_docs_search_engine
+ for name in ("mintlify_client", "docs_client"):
+ monkeypatch.setattr(
+ engine,
+ name,
+ _clone_with_transport(getattr(engine, name), mock_router.transport),
+ )
+ return mock_router
diff --git a/mcp_server/tests/helpers/__init__.py b/mcp_server/tests/helpers/__init__.py
new file mode 100644
index 0000000000..0fa7fb48bb
--- /dev/null
+++ b/mcp_server/tests/helpers/__init__.py
@@ -0,0 +1,49 @@
+"""Shared test helpers for the Prowler MCP Server suite.
+
+Import from the submodules directly (``from tests.helpers.jsonapi import ...``);
+this package only re-exports the surface so it is discoverable in one place.
+
+Nothing here is collected by pytest -- ``python_files`` is ``test_*.py``.
+"""
+
+from tests.helpers.assertions import (
+ NAMESPACES,
+ assert_namespaced,
+ assert_tool_contract,
+ tools_in_namespace,
+)
+from tests.helpers.http import MockRouter
+from tests.helpers.jsonapi import (
+ jsonapi_collection,
+ jsonapi_document,
+ jsonapi_error,
+ jsonapi_relationship_many,
+ jsonapi_relationship_one,
+ jsonapi_resource,
+ task_document,
+)
+from tests.helpers.tokens import (
+ FAKE_API_KEY,
+ FAKE_LEGACY_API_KEY,
+ MALFORMED_API_KEY,
+ fake_jwt,
+)
+
+__all__ = [
+ "FAKE_API_KEY",
+ "FAKE_LEGACY_API_KEY",
+ "MALFORMED_API_KEY",
+ "NAMESPACES",
+ "MockRouter",
+ "assert_namespaced",
+ "assert_tool_contract",
+ "fake_jwt",
+ "jsonapi_collection",
+ "jsonapi_document",
+ "jsonapi_error",
+ "jsonapi_relationship_many",
+ "jsonapi_relationship_one",
+ "jsonapi_resource",
+ "task_document",
+ "tools_in_namespace",
+]
diff --git a/mcp_server/tests/helpers/assertions.py b/mcp_server/tests/helpers/assertions.py
new file mode 100644
index 0000000000..06445cbaa9
--- /dev/null
+++ b/mcp_server/tests/helpers/assertions.py
@@ -0,0 +1,66 @@
+"""Assertions for the MCP tool contract every sub-server must honour.
+
+A tool's description and its parameter descriptions are not documentation -- they
+are the only thing a model sees when deciding whether and how to call it. A tool
+that registers without them is invisible in practice, so these are correctness
+assertions rather than style ones.
+"""
+
+from mcp.types import Tool
+
+# Mounted namespaces, most specific first so prefix matching is unambiguous.
+NAMESPACES = ("prowler_hub_", "prowler_docs_", "prowler_")
+
+
+def assert_tool_contract(tool: Tool) -> None:
+ """Assert the tool and all of its parameters carry a usable description.
+
+ Missing and blank are asserted separately because they are different
+ mistakes: a missing description was never written, a blank one exists but was
+ left empty. One truthiness check would report both the same way.
+ """
+ assert tool.description is not None, (
+ f"Tool '{tool.name}' has no description. Its docstring is what the model reads."
+ )
+ assert tool.description.strip(), (
+ f"Tool '{tool.name}' has a blank description. "
+ "Its docstring is what the model reads."
+ )
+
+ # `inputSchema` is a required field of the MCP Tool type, so it is always a
+ # dict; a tool that takes no arguments simply has no `properties`.
+ for parameter, schema in tool.inputSchema.get("properties", {}).items():
+ description = schema.get("description")
+ assert description is not None, (
+ f"Parameter '{parameter}' of tool '{tool.name}' has no description. "
+ "Declare it with pydantic Field(description=...)."
+ )
+ assert description.strip(), (
+ f"Parameter '{parameter}' of tool '{tool.name}' has a blank description. "
+ "Declare it with pydantic Field(description=...)."
+ )
+
+
+def assert_namespaced(tool: Tool) -> None:
+ """Assert the tool is reachable under one of the published namespaces."""
+ assert tool.name.startswith(NAMESPACES), (
+ f"Tool '{tool.name}' is outside the published namespaces {NAMESPACES}"
+ )
+
+
+def tools_in_namespace(tools: list[Tool], namespace: str) -> list[Tool]:
+ """Return the tools in a namespace.
+
+ ``prowler_`` is a prefix of the other two namespaces, so tools belonging to a
+ more specific one are excluded rather than counted twice.
+ """
+ more_specific = tuple(
+ other
+ for other in NAMESPACES
+ if other != namespace and other.startswith(namespace)
+ )
+ return [
+ tool
+ for tool in tools
+ if tool.name.startswith(namespace) and not tool.name.startswith(more_specific)
+ ]
diff --git a/mcp_server/tests/helpers/http.py b/mcp_server/tests/helpers/http.py
new file mode 100644
index 0000000000..b22994eac0
--- /dev/null
+++ b/mcp_server/tests/helpers/http.py
@@ -0,0 +1,118 @@
+"""Route registry and request recorder backed by ``httpx.MockTransport``.
+
+Mocking at the transport boundary rather than stubbing ``client.request`` keeps
+the parts of httpx the code under test actually relies on in play: base-URL
+joining, query-parameter encoding, header assembly, ``raise_for_status()`` and
+JSON decoding. A test that asserts on a recorded request is therefore asserting
+on the bytes that would really have gone out.
+"""
+
+import json
+from collections.abc import Callable
+from typing import Any
+
+import httpx
+
+_UNSET = object()
+
+ResponseFactory = Callable[[httpx.Request], httpx.Response]
+
+
+class MockRouter:
+ """Declare ``(METHOD, path) -> response`` and inspect what was requested.
+
+ Responses registered for the same route are consumed in order and the last
+ one repeats forever. That is what makes polling testable: register
+ ``executing``, ``executing``, ``completed`` and the loop sees each in turn.
+
+ An unregistered request raises instead of returning a default, so a test can
+ never silently exercise a different endpoint than the one it set up.
+ """
+
+ def __init__(self) -> None:
+ self._routes: dict[tuple[str, str], list[ResponseFactory]] = {}
+ self.requests: list[httpx.Request] = []
+
+ # --- registration -----------------------------------------------------
+
+ def add(
+ self,
+ method: str,
+ path: str,
+ *,
+ status: int = 200,
+ json: Any = _UNSET,
+ text: str | None = None,
+ headers: dict[str, str] | None = None,
+ ) -> "MockRouter":
+ """Register a canned response for a route. Chainable."""
+ kwargs: dict[str, Any] = {"headers": headers}
+ if json is not _UNSET:
+ kwargs["json"] = json
+ if text is not None:
+ kwargs["text"] = text
+ return self.add_handler(
+ method, path, lambda _request: httpx.Response(status, **kwargs)
+ )
+
+ def add_handler(
+ self, method: str, path: str, handler: ResponseFactory
+ ) -> "MockRouter":
+ """Register a callable that builds the response from the request."""
+ self._routes.setdefault((method.upper(), path), []).append(handler)
+ return self
+
+ # --- transport --------------------------------------------------------
+
+ @property
+ def transport(self) -> httpx.MockTransport:
+ """A transport that serves this router. Works for sync and async clients."""
+ return httpx.MockTransport(self._handle)
+
+ def _handle(self, request: httpx.Request) -> httpx.Response:
+ self.requests.append(request)
+ queue = self._routes.get((request.method.upper(), request.url.path))
+ if not queue:
+ registered = (
+ ", ".join(f"{method} {path}" for method, path in sorted(self._routes))
+ or "none"
+ )
+ raise AssertionError(
+ f"Unregistered request {request.method} {request.url}. "
+ f"Registered routes: {registered}"
+ )
+ # Keep the final response so a route can be polled repeatedly.
+ factory = queue.pop(0) if len(queue) > 1 else queue[0]
+ return factory(request)
+
+ # --- inspection -------------------------------------------------------
+
+ def request_for(self, method: str, path: str) -> httpx.Request:
+ """Return the last recorded request for a route, failing if there is none."""
+ matches = [
+ request
+ for request in self.requests
+ if request.method.upper() == method.upper() and request.url.path == path
+ ]
+ if not matches:
+ raise AssertionError(
+ f"No {method.upper()} {path} request was made. Made: {self.paths()}"
+ )
+ return matches[-1]
+
+ def query_params(self, method: str, path: str) -> dict[str, str]:
+ """Return the decoded query parameters of the last request for a route."""
+ return dict(self.request_for(method, path).url.params)
+
+ def json_body(self, method: str, path: str) -> Any:
+ """Return the decoded JSON body of the last request for a route.
+
+ Write tools build a JSON:API document by hand, and the API silently
+ ignores an attribute it does not recognise, so the body is the only place
+ a misspelled key shows up.
+ """
+ return json.loads(self.request_for(method, path).content)
+
+ def paths(self) -> list[str]:
+ """Return every request made so far, as ``"METHOD /path"`` strings."""
+ return [f"{request.method} {request.url.path}" for request in self.requests]
diff --git a/mcp_server/tests/helpers/jsonapi.py b/mcp_server/tests/helpers/jsonapi.py
new file mode 100644
index 0000000000..ac35d46e46
--- /dev/null
+++ b/mcp_server/tests/helpers/jsonapi.py
@@ -0,0 +1,112 @@
+"""Builders for the JSON:API documents the Prowler API returns.
+
+Every model's ``from_api_response()`` and every tool's error path consumes one of
+these shapes, so building them by hand in each test would duplicate the document
+structure hundreds of times. The builders keep the *shape* in one place so tests
+only express the part they actually care about.
+"""
+
+from typing import Any
+
+
+def jsonapi_relationship_many(resource_type: str, *ids: str) -> dict[str, Any]:
+ """Build a to-many relationship.
+
+ Passing no ids yields a present-but-empty relationship (``{"data": []}``),
+ which ``extract_relationship_ids`` reports as ``[]`` rather than ``None``.
+ """
+ return {"data": [{"type": resource_type, "id": resource_id} for resource_id in ids]}
+
+
+def jsonapi_relationship_one(resource_type: str, resource_id: str) -> dict[str, Any]:
+ """Build a to-one relationship."""
+ return {"data": {"type": resource_type, "id": resource_id}}
+
+
+def jsonapi_resource(
+ resource_type: str,
+ resource_id: str,
+ attributes: dict[str, Any] | None = None,
+ relationships: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ """Build a single JSON:API resource object.
+
+ ``relationships`` is omitted from the result entirely when not supplied, so a
+ test can express "the document did not expose this relationship"
+ (``extract_relationship_ids`` -> ``None``) distinctly from "the relationship
+ is present and empty" (-> ``[]``). Conflating the two is exactly the bug the
+ models go out of their way to avoid.
+ """
+ resource: dict[str, Any] = {
+ "type": resource_type,
+ "id": resource_id,
+ "attributes": attributes or {},
+ }
+ if relationships is not None:
+ resource["relationships"] = relationships
+ return resource
+
+
+def jsonapi_document(
+ data: dict[str, Any] | list[dict[str, Any]],
+ included: list[dict[str, Any]] | None = None,
+ meta: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ """Build a top-level JSON:API document."""
+ document: dict[str, Any] = {"data": data}
+ if included is not None:
+ document["included"] = included
+ if meta is not None:
+ document["meta"] = meta
+ return document
+
+
+def jsonapi_collection(
+ items: list[dict[str, Any]],
+ *,
+ page: int = 1,
+ pages: int = 1,
+ count: int | None = None,
+ included: list[dict[str, Any]] | None = None,
+) -> dict[str, Any]:
+ """Build a paginated collection document.
+
+ The ``meta.pagination`` keys are exactly the ones every ``*ListResponse``
+ reads (``page``, ``pages``, ``count``). ``count`` defaults to the number of
+ items so the common single-page case needs no arguments.
+ """
+ return jsonapi_document(
+ data=items,
+ included=included,
+ meta={
+ "pagination": {
+ "page": page,
+ "pages": pages,
+ "count": len(items) if count is None else count,
+ }
+ },
+ )
+
+
+def jsonapi_error(status: int, detail: str, title: str | None = None) -> dict[str, Any]:
+ """Build an error document.
+
+ ``ProwlerAPIClient._make_request`` surfaces ``errors[0].detail`` in the
+ exception message it raises, and tools relay that straight to the model.
+ """
+ error: dict[str, Any] = {"status": str(status), "detail": detail}
+ if title is not None:
+ error["title"] = title
+ return {"errors": [error]}
+
+
+def task_document(task_id: str, state: str, error: str | None = None) -> dict[str, Any]:
+ """Build a ``/tasks/{id}`` document for driving ``poll_task_until_complete``.
+
+ Register a sequence of these on a ``MockRouter`` route (for example
+ ``executing``, ``executing``, ``completed``) to exercise the polling loop.
+ """
+ attributes: dict[str, Any] = {"state": state}
+ if error is not None:
+ attributes["error"] = error
+ return jsonapi_document(jsonapi_resource("tasks", task_id, attributes))
diff --git a/mcp_server/tests/helpers/tokens.py b/mcp_server/tests/helpers/tokens.py
new file mode 100644
index 0000000000..93af70ad91
--- /dev/null
+++ b/mcp_server/tests/helpers/tokens.py
@@ -0,0 +1,34 @@
+"""Obviously-fake credentials for tests.
+
+Deliberately unrealistic so repository secret scanning does not flag them. Never
+put a value here that could be mistaken for a real key.
+"""
+
+import base64
+import json
+import time
+
+# Prowler API keys are recognised by their `pk_` prefix; anything else is rejected.
+FAKE_API_KEY = "pk_fake_api_key_for_unit_testing_only"
+FAKE_LEGACY_API_KEY = "pk_fake_legacy_api_key_for_unit_testing_only"
+MALFORMED_API_KEY = "not_a_prowler_api_key"
+
+
+def fake_jwt(expires_in: int = 3600, **claims: object) -> str:
+ """Mint an unsigned JWT whose ``exp`` is ``expires_in`` seconds from now.
+
+ Pass a negative ``expires_in`` for an already-expired token.
+
+ ``ProwlerAppAuth._parse_jwt`` only base64url-decodes the payload and reads
+ ``exp`` -- it never verifies the signature, because the Prowler API is what
+ validates the token. A placeholder signature is therefore enough, and avoids
+ adding a JWT library just for tests.
+ """
+
+ def _segment(payload: dict[str, object]) -> str:
+ raw = json.dumps(payload, separators=(",", ":")).encode()
+ return base64.urlsafe_b64encode(raw).decode().rstrip("=")
+
+ header = _segment({"alg": "HS256", "typ": "JWT"})
+ body = _segment({"exp": int(time.time()) + expires_in, **claims})
+ return f"{header}.{body}.fake-signature-not-verified"
diff --git a/mcp_server/tests/prowler_app/__init__.py b/mcp_server/tests/prowler_app/__init__.py
new file mode 100644
index 0000000000..255373895e
--- /dev/null
+++ b/mcp_server/tests/prowler_app/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler App sub-server."""
diff --git a/mcp_server/tests/prowler_app/models/__init__.py b/mcp_server/tests/prowler_app/models/__init__.py
new file mode 100644
index 0000000000..586e285982
--- /dev/null
+++ b/mcp_server/tests/prowler_app/models/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler App Pydantic models."""
diff --git a/mcp_server/tests/prowler_app/models/test_findings.py b/mcp_server/tests/prowler_app/models/test_findings.py
new file mode 100644
index 0000000000..3556201012
--- /dev/null
+++ b/mcp_server/tests/prowler_app/models/test_findings.py
@@ -0,0 +1,205 @@
+"""Tests for the security finding models.
+
+Reference for later branches: build the API document with the ``jsonapi``
+helpers, run it through ``from_api_response()``, then assert on both the model
+and its ``model_dump()``. The dump is what the agent actually receives, and
+``MinimalSerializerMixin`` makes the two differ.
+"""
+
+from prowler_mcp_server.prowler_app.models.findings import (
+ DetailedFinding,
+ FindingsListResponse,
+ FindingsOverview,
+ SimplifiedFinding,
+)
+from tests.helpers.jsonapi import (
+ jsonapi_collection,
+ jsonapi_relationship_many,
+ jsonapi_relationship_one,
+ jsonapi_resource,
+)
+
+CHECK_METADATA = {
+ "checkid": "s3_bucket_public_access",
+ "checktitle": "Ensure S3 buckets block public access",
+ "description": "Checks whether the bucket blocks public access.",
+ "provider": "aws",
+ "servicename": "s3",
+ "resourcetype": "AwsS3Bucket",
+ "risk": "Public buckets expose data to the internet.",
+ "additionalurls": ["https://docs.aws.amazon.com/s3/"],
+ "categories": ["encryption", "internet-exposed"],
+}
+
+FINDING_ATTRIBUTES = {
+ "uid": "prowler-aws-s3_bucket_public_access-123456789012-us-east-1-my-bucket",
+ "status": "FAIL",
+ "severity": "high",
+ "status_extended": "S3 bucket my-bucket is publicly accessible.",
+ "delta": "new",
+ "muted": False,
+ "muted_reason": None,
+ "check_metadata": CHECK_METADATA,
+}
+
+DETAILED_ATTRIBUTES = {
+ **FINDING_ATTRIBUTES,
+ "inserted_at": "2025-01-15T10:00:00Z",
+ "updated_at": "2025-01-15T10:00:00Z",
+ "first_seen_at": "2025-01-10T09:00:00Z",
+}
+
+
+def test_simplified_finding_lifts_the_check_id_out_of_the_check_metadata():
+ """`check_id` is nested under `check_metadata.checkid` in the API document.
+
+ Flattening it is what lets an agent filter findings by check without being
+ handed the whole metadata blob for every row in a list.
+ """
+ finding = SimplifiedFinding.from_api_response(
+ jsonapi_resource("findings", "f1", FINDING_ATTRIBUTES)
+ )
+
+ assert finding.check_id == "s3_bucket_public_access"
+ assert finding.severity == "high"
+ assert finding.status == "FAIL"
+
+
+def test_empty_finding_fields_are_dropped_from_the_serialized_payload():
+ """Empty values are removed to keep the payload small for the model.
+
+ `muted_reason` is None on an unmuted finding; emitting it would spend tokens
+ on every row of every list response to say nothing.
+ """
+ finding = SimplifiedFinding.from_api_response(
+ jsonapi_resource("findings", "f1", FINDING_ATTRIBUTES)
+ )
+
+ dumped = finding.model_dump()
+
+ assert "muted_reason" not in dumped
+ assert dumped["uid"] == FINDING_ATTRIBUTES["uid"]
+
+
+def test_detailed_finding_parses_both_relationship_shapes():
+ """`scan` is a to-one relationship and `resources` is to-many.
+
+ They are read from the same `relationships` object but reduce to a single id
+ and a list of ids respectively.
+ """
+ resource = jsonapi_resource(
+ "findings",
+ "f1",
+ attributes=DETAILED_ATTRIBUTES,
+ relationships={
+ "scan": jsonapi_relationship_one("scans", "s1"),
+ "resources": jsonapi_relationship_many("resources", "r1", "r2"),
+ },
+ )
+
+ finding = DetailedFinding.from_api_response(resource)
+
+ assert finding.scan_id == "s1"
+ assert finding.resource_ids == ["r1", "r2"]
+
+
+def test_detailed_finding_tolerates_missing_relationships():
+ """A document without relationships must not raise.
+
+ `get_finding_details` requests `include=scan,resources`, but a finding whose
+ scan has been pruned still has to render rather than fail the tool call.
+ """
+ finding = DetailedFinding.from_api_response(
+ jsonapi_resource("findings", "f1", DETAILED_ATTRIBUTES)
+ )
+
+ assert finding.scan_id is None
+ assert finding.resource_ids == []
+
+
+def test_detailed_finding_flattens_the_nested_remediation_guidance():
+ """Remediation is the payload an agent needs to actually fix the finding.
+
+ The API nests it under `remediation.code.*` and `remediation.recommendation.text`;
+ the model flattens both into one object.
+ """
+ attributes = {
+ **DETAILED_ATTRIBUTES,
+ "check_metadata": {
+ **CHECK_METADATA,
+ "remediation": {
+ "code": {
+ "cli": "aws s3api put-public-access-block ...",
+ "terraform": 'resource "aws_s3_bucket_public_access_block" ...',
+ "nativeiac": "",
+ "other": "",
+ },
+ "recommendation": {"text": "Block all public access on the bucket."},
+ },
+ },
+ }
+
+ finding = DetailedFinding.from_api_response(
+ jsonapi_resource("findings", "f1", attributes)
+ )
+
+ remediation = finding.check_metadata.remediation
+ assert remediation.cli.startswith("aws s3api")
+ assert remediation.recommendation == "Block all public access on the bucket."
+ # Empty code snippets are dropped rather than shown as blank fields.
+ assert "nativeiac" not in remediation.model_dump()
+
+
+def test_check_metadata_without_remediation_is_left_unset():
+ """Not every check ships remediation guidance; absence must not fabricate one."""
+ finding = DetailedFinding.from_api_response(
+ jsonapi_resource("findings", "f1", DETAILED_ATTRIBUTES)
+ )
+
+ assert finding.check_metadata.remediation is None
+ assert "remediation" not in finding.check_metadata.model_dump()
+
+
+def test_list_response_carries_the_api_pagination_metadata():
+ """Pagination tells an agent whether it has seen everything it asked for."""
+ response = jsonapi_collection(
+ [jsonapi_resource("findings", "f1", FINDING_ATTRIBUTES)],
+ page=2,
+ pages=7,
+ count=312,
+ )
+
+ result = FindingsListResponse.from_api_response(response)
+
+ assert result.current_page == 2
+ assert result.total_num_pages == 7
+ assert result.total_num_finding == 312
+ assert result.findings[0].check_id == "s3_bucket_public_access"
+
+
+def test_overview_renames_the_pass_attribute_to_a_valid_identifier():
+ """The API's `pass` count cannot keep its name -- `pass` is a Python keyword."""
+ response = jsonapi_resource(
+ "findings-overview",
+ "overview",
+ {
+ "total": 100,
+ "fail": 30,
+ "pass": 60,
+ "muted": 10,
+ "new": 5,
+ "changed": 3,
+ "fail_new": 2,
+ "fail_changed": 1,
+ "pass_new": 2,
+ "pass_changed": 1,
+ "muted_new": 1,
+ "muted_changed": 1,
+ },
+ )
+
+ overview = FindingsOverview.from_api_response({"data": response})
+
+ assert overview.passed == 60
+ assert overview.fail == 30
+ assert overview.total == 100
diff --git a/mcp_server/tests/prowler_app/models/test_integrations.py b/mcp_server/tests/prowler_app/models/test_integrations.py
new file mode 100644
index 0000000000..c62646866d
--- /dev/null
+++ b/mcp_server/tests/prowler_app/models/test_integrations.py
@@ -0,0 +1,280 @@
+"""Tests for the integration models.
+
+Two things here are not ordinary serialization and carry the weight of the
+module: the Security Hub ``regions`` map, which is rewritten into the far smaller
+``enabled_regions`` list before an agent ever sees it, and the Jira dispatch
+result, whose ``safe_to_retry`` flag is the only thing standing between a
+half-finished dispatch and a project full of duplicated work items.
+"""
+
+import pytest
+
+from prowler_mcp_server.prowler_app.models.integrations import (
+ DetailedIntegration,
+ IntegrationConnectionStatus,
+ IntegrationsListResponse,
+ JiraDispatchResult,
+ JiraIssueTypes,
+ SimplifiedIntegration,
+)
+from tests.helpers.jsonapi import (
+ jsonapi_collection,
+ jsonapi_relationship_many,
+ jsonapi_resource,
+)
+
+S3_ATTRIBUTES = {
+ "integration_type": "amazon_s3",
+ "enabled": True,
+ "connected": True,
+ "connection_last_checked_at": "2025-01-15T10:00:00Z",
+ "inserted_at": "2025-01-10T09:00:00Z",
+ "updated_at": "2025-01-15T10:00:00Z",
+ "configuration": {"bucket_name": "my-reports", "output_directory": "prowler"},
+}
+
+SECURITY_HUB_ATTRIBUTES = {
+ "integration_type": "aws_security_hub",
+ "enabled": True,
+ "connected": True,
+ "configuration": {
+ "send_only_fails": True,
+ "archive_previous_findings": False,
+ "regions": {"us-east-1": True, "eu-west-1": False, "eu-west-3": True},
+ },
+}
+
+JIRA_ATTRIBUTES = {
+ "integration_type": "jira",
+ "enabled": True,
+ "connected": None,
+ "configuration": {"domain": "acme", "projects": {}, "issue_types": {}},
+}
+
+
+def test_simplified_integration_lifts_the_attached_provider_ids():
+ """`provider_ids` comes from the relationship linkage, not the attributes.
+
+ It is what tells an agent whether an integration covers the account it is
+ looking at, so reading it out of the wrong place silently scopes every
+ integration to the whole tenant.
+ """
+ integration = SimplifiedIntegration.from_api_response(
+ jsonapi_resource(
+ "integrations",
+ "i1",
+ S3_ATTRIBUTES,
+ relationships={
+ "providers": jsonapi_relationship_many("providers", "p1", "p2")
+ },
+ )
+ )
+
+ assert integration.provider_ids == ["p1", "p2"]
+ assert integration.integration_type == "amazon_s3"
+
+
+def test_a_never_checked_integration_still_reports_its_connected_field():
+ """`connected: null` means "never checked", which is not "not connected".
+
+ Every other empty value is dropped to save tokens, so without the override
+ this field would vanish exactly when its absence is most misleading.
+ """
+ integration = SimplifiedIntegration.from_api_response(
+ jsonapi_resource("integrations", "i1", {**JIRA_ATTRIBUTES, "connected": None})
+ )
+
+ dumped = integration.model_dump()
+
+ assert dumped["connected"] is None
+ # Contrast: an untouched empty field is dropped
+ assert "connection_last_checked_at" not in dumped
+
+
+def test_the_list_view_drops_a_configuration_the_api_still_sends():
+ """The sparse fieldset asks the API to leave `configuration` out.
+
+ The model must drop it anyway rather than pass it through: the fieldset is a
+ request, not a guarantee, and a Jira configuration listing every project of
+ the site is exactly what the separate detailed view exists to hold back.
+ """
+ integration = SimplifiedIntegration.from_api_response(
+ jsonapi_resource("integrations", "i1", JIRA_ATTRIBUTES)
+ )
+
+ assert "configuration" not in integration.model_dump()
+
+
+def test_security_hub_regions_are_collapsed_into_the_enabled_ones():
+ """The API returns every region of the partition with a boolean.
+
+ Only the enabled ones carry information, so the map is rewritten as a sorted
+ list. Passing the raw map through would spend tokens listing dozens of
+ regions to say "no".
+ """
+ integration = DetailedIntegration.from_api_response(
+ jsonapi_resource("integrations", "i1", SECURITY_HUB_ATTRIBUTES)
+ )
+
+ assert integration.configuration["enabled_regions"] == ["eu-west-3", "us-east-1"]
+ assert "regions" not in integration.configuration
+
+
+def test_an_unexpected_regions_shape_is_preserved_rather_than_dropped():
+ """A shape the rewrite does not understand is kept verbatim.
+
+ Silently dropping it would hide a real API change behind an integration that
+ merely looks like it has no regions enabled.
+ """
+ attributes = {
+ **SECURITY_HUB_ATTRIBUTES,
+ "configuration": {"regions": ["us-east-1"]},
+ }
+
+ integration = DetailedIntegration.from_api_response(
+ jsonapi_resource("integrations", "i1", attributes)
+ )
+
+ assert integration.configuration["regions"] == ["us-east-1"]
+ assert "enabled_regions" not in integration.configuration
+
+
+def test_a_non_security_hub_configuration_is_passed_through_untouched():
+ """Only Security Hub has a configuration worth rewriting."""
+ integration = DetailedIntegration.from_api_response(
+ jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)
+ )
+
+ assert integration.configuration == S3_ATTRIBUTES["configuration"]
+
+
+def test_the_list_response_reports_the_pagination_of_the_whole_query():
+ """Counts come from `meta.pagination`, not from the length of this page."""
+ response = IntegrationsListResponse.from_api_response(
+ jsonapi_collection(
+ [jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)],
+ page=2,
+ pages=3,
+ count=7,
+ )
+ )
+
+ assert [integration.id for integration in response.integrations] == ["i1"]
+ assert (response.total_num_integrations, response.total_num_pages) == (7, 3)
+ assert response.current_page == 2
+
+
+@pytest.mark.parametrize(
+ ("connected", "expected"),
+ [(True, "connected"), (False, "failed"), (None, "not_tested")],
+)
+def test_the_connection_check_maps_its_tri_state_onto_a_readable_outcome(
+ connected, expected
+):
+ """`null` is "the check did not run", which is not the same as a failure.
+
+ Collapsing it onto `failed` would send an agent chasing credentials that were
+ never actually tested.
+ """
+ status = IntegrationConnectionStatus.create(
+ jsonapi_resource("integrations", "i1", S3_ATTRIBUTES),
+ {"connected": connected},
+ )
+
+ assert status.connected == expected
+
+
+def test_an_unreadable_connection_result_raises_instead_of_guessing():
+ """Anything other than a boolean or null is an API change, not a failure."""
+ with pytest.raises(ValueError, match="unexpected connection check result"):
+ IntegrationConnectionStatus.create(
+ jsonapi_resource("integrations", "i1", S3_ATTRIBUTES),
+ {"connected": "yes"},
+ )
+
+
+def test_the_connection_error_is_only_reported_when_there_is_one():
+ """A successful check must not carry an empty `error` key."""
+ status = IntegrationConnectionStatus.create(
+ jsonapi_resource("integrations", "i1", S3_ATTRIBUTES), {"connected": True}
+ )
+
+ assert "error" not in status.model_dump()
+
+
+def test_jira_issue_types_are_read_from_a_wrapped_or_a_bare_payload():
+ """This endpoint returns a non-model resource, so both shapes must work."""
+ wrapped = JiraIssueTypes.from_api_response(
+ jsonapi_resource(
+ "jira-issue-types", "i1", {"project_key": "PROJ", "issue_types": ["Task"]}
+ )
+ )
+ bare = JiraIssueTypes.from_api_response(
+ {"project_key": "PROJ", "issue_types": ["Task"]}
+ )
+
+ assert (
+ wrapped.model_dump()
+ == bare.model_dump()
+ == {
+ "project_key": "PROJ",
+ "issue_types": ["Task"],
+ }
+ )
+
+
+def test_an_unreadable_issue_types_payload_raises():
+ """Returning an empty list would read as "this project has no issue types"."""
+ with pytest.raises(ValueError, match="unexpected Jira issue types payload"):
+ JiraIssueTypes.from_api_response({"project_key": "PROJ"})
+
+
+def test_a_dispatch_that_created_nothing_is_the_only_one_safe_to_retry():
+ """Work items are created one by one and Prowler cannot delete them.
+
+ So a retry is only safe when the run provably created none. Anything else
+ duplicates work items in a project a human then has to clean up.
+ """
+ empty = JiraDispatchResult.from_task_result({"created_count": 0, "failed_count": 3})
+ partial = JiraDispatchResult.from_task_result(
+ {"created_count": 1, "failed_count": 2}
+ )
+
+ assert empty.safe_to_retry is True
+ assert partial.safe_to_retry is False
+
+
+def test_a_zero_count_survives_serialization():
+ """Zero created work items is an outcome; an unknown count is not.
+
+ The minimal serializer drops empty values, so without the override a fully
+ failed dispatch would report no counts at all.
+ """
+ dumped = JiraDispatchResult.from_task_result(
+ {"created_count": 0, "failed_count": 3}
+ ).model_dump()
+
+ assert dumped["created_count"] == 0
+ assert dumped["failed_count"] == 3
+ assert dumped["status"] == "completed"
+
+
+@pytest.mark.parametrize(
+ "result",
+ [
+ {"failed_count": 2},
+ {"created_count": 1},
+ {"created_count": "1", "failed_count": 0},
+ None,
+ "done",
+ ],
+ ids=["no-created", "no-failed", "not-an-int", "null", "not-an-object"],
+)
+def test_a_dispatch_result_without_usable_counters_raises(result):
+ """Defaulting the counters to zero would report the run as safe to retry.
+
+ That is the one wrong answer here: it invites a second dispatch on top of
+ work items that may already exist.
+ """
+ with pytest.raises(ValueError, match="dispatch task did not report"):
+ JiraDispatchResult.from_task_result(result)
diff --git a/mcp_server/tests/prowler_app/models/test_utils.py b/mcp_server/tests/prowler_app/models/test_utils.py
new file mode 100644
index 0000000000..b51524a9ea
--- /dev/null
+++ b/mcp_server/tests/prowler_app/models/test_utils.py
@@ -0,0 +1,57 @@
+"""Tests for the shared JSON:API response-parsing helpers.
+
+These back every model's ``from_api_response()``, so they are foundation-level
+rather than tied to any one feature.
+"""
+
+from prowler_mcp_server.prowler_app.models.utils import extract_relationship_ids
+from tests.helpers.jsonapi import jsonapi_relationship_many, jsonapi_relationship_one
+
+
+def test_an_absent_relationship_is_unknown_rather_than_empty():
+ """A relationship the document never mentioned yields None, not [].
+
+ Returning [] would tell an agent "this role is assigned to nobody" when the
+ serializer simply did not expose the relationship -- for example a role
+ included via `?include=roles`, which carries no `users`.
+ """
+ assert extract_relationship_ids({}, "users") is None
+
+
+def test_a_present_but_empty_relationship_is_explicitly_empty():
+ """An empty relationship yields [], which genuinely means "none"."""
+ relationships = {"users": jsonapi_relationship_many("users")}
+
+ assert extract_relationship_ids(relationships, "users") == []
+
+
+def test_a_to_many_relationship_is_flattened_to_its_ids():
+ """Linkage objects are reduced to the plain ids the tools pass around."""
+ relationships = {"users": jsonapi_relationship_many("users", "u1", "u2")}
+
+ assert extract_relationship_ids(relationships, "users") == ["u1", "u2"]
+
+
+def test_a_to_one_relationship_is_returned_as_a_single_element_list():
+ """To-one and to-many both return a list so callers need no shape check."""
+ relationships = {"scan": jsonapi_relationship_one("scans", "s1")}
+
+ assert extract_relationship_ids(relationships, "scan") == ["s1"]
+
+
+def test_a_null_to_one_relationship_is_empty():
+ """An explicitly null to-one link means "not related", not "unknown"."""
+ relationships = {"scan": {"data": None}}
+
+ assert extract_relationship_ids(relationships, "scan") == []
+
+
+def test_members_without_an_id_are_discarded():
+ """Malformed linkage must not surface as a None entry in the id list.
+
+ A None id would flow into a tool's next request and produce a confusing
+ 404 rather than a clean, short list.
+ """
+ relationships = {"users": {"data": [{"type": "users", "id": "u1"}, {}]}}
+
+ assert extract_relationship_ids(relationships, "users") == ["u1"]
diff --git a/mcp_server/tests/prowler_app/tools/__init__.py b/mcp_server/tests/prowler_app/tools/__init__.py
new file mode 100644
index 0000000000..2a8a7db94c
--- /dev/null
+++ b/mcp_server/tests/prowler_app/tools/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler App MCP tools."""
diff --git a/mcp_server/tests/prowler_app/tools/test_findings.py b/mcp_server/tests/prowler_app/tools/test_findings.py
new file mode 100644
index 0000000000..b1e1a3aeb3
--- /dev/null
+++ b/mcp_server/tests/prowler_app/tools/test_findings.py
@@ -0,0 +1,347 @@
+"""Tests for the security findings tools.
+
+Reference for later branches. Drive tools through an in-memory MCP client by
+default. Tool parameters are declared with pydantic ``Field(default=...)``, and
+those defaults are only resolved by FastMCP's tool wrapper -- calling the method
+directly leaves an omitted argument as a raw ``FieldInfo`` object, which is
+truthy and silently produces nonsense filters. Call the method directly only when
+passing every argument explicitly.
+
+Everything here relies on ``mock_api_client`` patching the API client *in place*:
+the tool instances captured that exact object when the package was imported, so a
+freshly-constructed client would not reach them.
+"""
+
+import pytest
+from fastmcp import Client
+
+from tests.helpers.jsonapi import (
+ jsonapi_collection,
+ jsonapi_error,
+ jsonapi_relationship_one,
+ jsonapi_resource,
+)
+
+LATEST = "/api/v1/findings/latest"
+HISTORICAL = "/api/v1/findings"
+
+CHECK_METADATA = {
+ "checkid": "s3_bucket_public_access",
+ "checktitle": "Ensure S3 buckets block public access",
+ "description": "Checks whether the bucket blocks public access.",
+ "provider": "aws",
+ "servicename": "s3",
+ "resourcetype": "AwsS3Bucket",
+ "risk": "Public buckets expose data to the internet.",
+ "additionalurls": [],
+ "categories": ["internet-exposed"],
+}
+
+FINDING_ATTRIBUTES = {
+ "uid": "prowler-aws-s3_bucket_public_access-123456789012-us-east-1-my-bucket",
+ "status": "FAIL",
+ "severity": "high",
+ "status_extended": "S3 bucket my-bucket is publicly accessible.",
+ "delta": "new",
+ "muted": False,
+ "muted_reason": None,
+ "check_metadata": CHECK_METADATA,
+}
+
+
+async def test_search_without_dates_queries_the_latest_scan_endpoint(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """With no date range the tool targets `/findings/latest`.
+
+ That endpoint reads only the most recent completed scan, which is far cheaper
+ than a historical query -- so picking the wrong one is a performance
+ regression the response body alone would not reveal.
+ """
+ mock_router.add(
+ "GET",
+ LATEST,
+ json=jsonapi_collection(
+ [jsonapi_resource("findings", "f1", FINDING_ATTRIBUTES)]
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool("prowler_search_security_findings", {})
+
+ assert result.data["findings"][0]["check_id"] == "s3_bucket_public_access"
+ assert mock_router.paths() == [f"GET {LATEST}"]
+
+
+async def test_search_defaults_to_failed_findings_only(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The default filter is FAIL, so an unqualified search surfaces real issues.
+
+ Also pins the sort order and field selection, which together keep the
+ response small and severity-first.
+ """
+ mock_router.add("GET", LATEST, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool("prowler_search_security_findings", {})
+
+ params = mock_router.query_params("GET", LATEST)
+ assert params["filter[status__in]"] == "FAIL"
+ assert params["sort"] == "severity,-inserted_at"
+ assert params["page[size]"] == "50"
+
+
+async def test_search_with_dates_switches_to_the_historical_endpoint(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A date range moves the query to `/findings` with an inserted_at window.
+
+ Supplying only `date_from` auto-completes the other boundary, so the caller
+ cannot accidentally request an unbounded historical scan.
+ """
+ mock_router.add("GET", HISTORICAL, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_search_security_findings", {"date_from": "2025-01-15"}
+ )
+
+ params = mock_router.query_params("GET", HISTORICAL)
+ assert params["filter[inserted_at__gte]"] == "2025-01-15"
+ assert params["filter[inserted_at__lte]"] == "2025-01-16"
+
+
+async def test_search_rejects_a_date_range_wider_than_the_api_allows(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The API caps historical queries at two days; reject before the round trip."""
+ async with Client(mcp_root_server) as client:
+ with pytest.raises(Exception, match="Date range cannot exceed 2 days"):
+ await client.call_tool(
+ "prowler_search_security_findings",
+ {"date_from": "2025-01-01", "date_to": "2025-01-10"},
+ )
+
+ assert mock_router.requests == []
+
+
+async def test_search_encodes_list_filters_as_comma_separated_values(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Multi-value filters reach the API as CSV, not as repeated query keys."""
+ mock_router.add("GET", LATEST, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_search_security_findings",
+ {"severity": ["critical", "high"], "service": ["s3", "ec2"]},
+ )
+
+ params = mock_router.query_params("GET", LATEST)
+ assert params["filter[severity__in]"] == "critical,high"
+ assert params["filter[service__in]"] == "s3,ec2"
+
+
+@pytest.mark.parametrize(
+ ("argument", "value", "expected_key", "expected_value"),
+ [
+ ("provider_type", ["aws", "gcp"], "filter[provider_type__in]", "aws,gcp"),
+ ("provider_alias", "prod", "filter[provider_alias__icontains]", "prod"),
+ ("region", ["us-east-1"], "filter[region__in]", "us-east-1"),
+ ("resource_type", ["AwsS3Bucket"], "filter[resource_type__in]", "AwsS3Bucket"),
+ (
+ "check_id",
+ ["s3_bucket_public_access"],
+ "filter[check_id__in]",
+ "s3_bucket_public_access",
+ ),
+ ("delta", ["new"], "filter[delta__in]", "new"),
+ ("search", "bucket", "filter[search]", "bucket"),
+ ],
+)
+async def test_search_maps_each_argument_onto_its_api_filter(
+ mcp_root_server,
+ mock_api_client,
+ mock_router,
+ argument,
+ value,
+ expected_key,
+ expected_value,
+):
+ """Every search argument maps to a specific API filter key.
+
+ A mistyped filter key is not an error the API reports -- it is simply ignored,
+ so the tool returns unfiltered results while appearing to work. Pinning the
+ exact key per argument is the only thing that catches that.
+ """
+ mock_router.add("GET", LATEST, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool("prowler_search_security_findings", {argument: value})
+
+ assert mock_router.query_params("GET", LATEST)[expected_key] == expected_value
+
+
+async def test_overview_can_be_scoped_to_a_provider(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The aggregate report accepts the same provider filter as the search tool."""
+ mock_router.add(
+ "GET",
+ "/api/v1/overviews/findings",
+ json={
+ "data": jsonapi_resource(
+ "findings-overview",
+ "overview",
+ dict.fromkeys(
+ [
+ "total",
+ "fail",
+ "pass",
+ "muted",
+ "new",
+ "changed",
+ "fail_new",
+ "fail_changed",
+ "pass_new",
+ "pass_changed",
+ "muted_new",
+ "muted_changed",
+ ],
+ 0,
+ ),
+ )
+ },
+ )
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_get_findings_overview", {"provider_type": ["aws"]}
+ )
+
+ params = mock_router.query_params("GET", "/api/v1/overviews/findings")
+ assert params["filter[provider_type__in]"] == "aws"
+
+
+async def test_search_normalises_a_string_muted_flag_to_a_boolean(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """`muted` accepts a string because some MCP clients send booleans as text.
+
+ It still has to reach the API as a lowercase boolean, otherwise the filter is
+ silently ignored and the agent gets muted findings it asked to exclude.
+ """
+ mock_router.add("GET", LATEST, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool("prowler_search_security_findings", {"muted": "true"})
+
+ assert mock_router.query_params("GET", LATEST)["filter[muted]"] == "true"
+
+
+async def test_search_rejects_an_out_of_range_page_size(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Page size is validated locally, saving a round trip on an obvious mistake."""
+ async with Client(mcp_root_server) as client:
+ with pytest.raises(Exception, match="Must be between 1 and 1000"):
+ await client.call_tool(
+ "prowler_search_security_findings", {"page_size": 5000}
+ )
+
+ assert mock_router.requests == []
+
+
+async def test_get_finding_details_requests_its_relationships(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Details are only useful with the scan and resources included.
+
+ Dropping the `include` would leave `scan_id` and `resource_ids` empty and the
+ agent unable to pivot from a finding to the resource it concerns.
+ """
+ attributes = {
+ **FINDING_ATTRIBUTES,
+ "inserted_at": "2025-01-15T10:00:00Z",
+ "updated_at": "2025-01-15T10:00:00Z",
+ }
+ mock_router.add(
+ "GET",
+ f"{HISTORICAL}/f1",
+ json={
+ "data": jsonapi_resource(
+ "findings",
+ "f1",
+ attributes,
+ relationships={"scan": jsonapi_relationship_one("scans", "s1")},
+ )
+ },
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_get_finding_details", {"finding_id": "f1"}
+ )
+
+ assert result.data["scan_id"] == "s1"
+ assert mock_router.query_params("GET", f"{HISTORICAL}/f1")["include"] == (
+ "scan,resources"
+ )
+
+
+async def test_get_finding_details_surfaces_the_api_error_detail(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A missing finding surfaces the API's message rather than an opaque failure."""
+ mock_router.add(
+ "GET", f"{HISTORICAL}/nope", status=404, json=jsonapi_error(404, "Not found.")
+ )
+
+ async with Client(mcp_root_server) as client:
+ with pytest.raises(Exception, match="Not found."):
+ await client.call_tool(
+ "prowler_get_finding_details", {"finding_id": "nope"}
+ )
+
+
+async def test_overview_renders_a_markdown_report_with_percentages(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The overview returns prose, not a model, so the arithmetic is the contract.
+
+ Percentages are derived here rather than by the API, which makes them the one
+ part of this tool that can silently go wrong.
+ """
+ mock_router.add(
+ "GET",
+ "/api/v1/overviews/findings",
+ json={
+ "data": jsonapi_resource(
+ "findings-overview",
+ "overview",
+ {
+ "total": 200,
+ "fail": 50,
+ "pass": 130,
+ "muted": 20,
+ "new": 10,
+ "changed": 4,
+ "fail_new": 6,
+ "fail_changed": 2,
+ "pass_new": 3,
+ "pass_changed": 1,
+ "muted_new": 1,
+ "muted_changed": 1,
+ },
+ )
+ },
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool("prowler_get_findings_overview", {})
+
+ report = result.data["report"]
+ assert "**Total Findings**: 200" in report
+ assert "**Failed Checks**: 50 (25.0%)" in report
+ assert "**Unchanged**: 186" in report
diff --git a/mcp_server/tests/prowler_app/tools/test_integrations.py b/mcp_server/tests/prowler_app/tools/test_integrations.py
new file mode 100644
index 0000000000..9d165a60c6
--- /dev/null
+++ b/mcp_server/tests/prowler_app/tools/test_integrations.py
@@ -0,0 +1,1131 @@
+"""Tests for the integrations tools.
+
+These tools are the only write surface in the MCP server that reaches a system
+Prowler does not own -- an S3 bucket, a Security Hub account, a Jira project --
+so what goes out on the wire matters as much as what comes back. Three things
+drive most of the assertions here:
+
+* Creating or updating an integration is a multi-request choreography (write,
+ connection check, task poll, re-read). ``mock_router.paths()`` is what pins it;
+ a skipped connection check leaves a Jira integration with no discovered
+ projects and is invisible in the response body.
+* The API *replaces* credentials and configuration wholesale, so the guards that
+ refuse a partial payload are protecting stored secrets, not just being tidy.
+* A Jira dispatch creates work items one at a time and Prowler cannot delete
+ them, so ``safe_to_retry`` must never be optimistic.
+
+As in ``test_findings``, tools are driven through an in-memory MCP client so
+FastMCP resolves the pydantic ``Field`` defaults.
+"""
+
+import httpx
+import pytest
+from fastmcp import Client
+
+from tests.helpers.http import MockRouter
+from tests.helpers.jsonapi import (
+ jsonapi_collection,
+ jsonapi_document,
+ jsonapi_error,
+ jsonapi_relationship_many,
+ jsonapi_resource,
+)
+
+INTEGRATIONS = "/api/v1/integrations"
+INTEGRATION = f"{INTEGRATIONS}/i1"
+CONNECTION = f"{INTEGRATION}/connection"
+DISPATCHES = f"{INTEGRATION}/jira/dispatches"
+ISSUE_TYPES = f"{INTEGRATION}/jira/issue_types"
+TASK = "/api/v1/tasks/t1"
+
+S3_ATTRIBUTES = {
+ "integration_type": "amazon_s3",
+ "enabled": True,
+ "connected": True,
+ "connection_last_checked_at": "2025-01-15T10:00:00Z",
+ "configuration": {"bucket_name": "my-reports", "output_directory": "prowler"},
+}
+
+SECURITY_HUB_ATTRIBUTES = {
+ "integration_type": "aws_security_hub",
+ "enabled": True,
+ "connected": True,
+ "configuration": {
+ "send_only_fails": False,
+ "archive_previous_findings": True,
+ "regions": {"us-east-1": True, "eu-west-1": False},
+ },
+}
+
+JIRA_ATTRIBUTES = {
+ "integration_type": "jira",
+ "enabled": True,
+ "connected": True,
+ "configuration": {
+ "domain": "acme",
+ "projects": {"PROJ": "Security"},
+ "issue_types": {"PROJ": ["Task", "Bug"]},
+ },
+}
+
+
+def stub_integration(
+ mock_router: MockRouter,
+ attributes: dict,
+ *,
+ provider_ids: tuple[str, ...] = (),
+) -> MockRouter:
+ """Serve ``GET /integrations/i1`` for every read a tool makes.
+
+ A single registration is enough because the router repeats its last response,
+ and the tools read the integration both before and after a write. Call it
+ twice to serve a different state to each read, which is what tells the state
+ returned after a write apart from the one read before it.
+ """
+ relationships = (
+ {"providers": jsonapi_relationship_many("providers", *provider_ids)}
+ if provider_ids
+ else None
+ )
+ return mock_router.add(
+ "GET",
+ INTEGRATION,
+ json=jsonapi_document(
+ jsonapi_resource("integrations", "i1", attributes, relationships)
+ ),
+ )
+
+
+def stub_connection_check(
+ mock_router: MockRouter, *, connected: bool = True, error: str | None = None
+) -> MockRouter:
+ """Serve the check as Prowler runs it: a POST that returns a task to poll."""
+ result: dict = {"connected": connected}
+ if error is not None:
+ result["error"] = error
+
+ mock_router.add(
+ "POST", CONNECTION, json=jsonapi_document(jsonapi_resource("tasks", "t1", {}))
+ )
+ return mock_router.add(
+ "GET",
+ TASK,
+ json=jsonapi_document(
+ jsonapi_resource("tasks", "t1", {"state": "completed", "result": result})
+ ),
+ )
+
+
+# ------------------------------------------------------------------ read tools
+
+
+async def test_listing_does_not_ask_for_the_configuration(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The sparse fieldset is what keeps a list of integrations small.
+
+ A Jira configuration carries every project and every issue type of the site,
+ which is the bulk of the payload and useless until an agent has picked one
+ integration to work with -- that is what prowler_get_integration is for.
+ """
+ mock_router.add("GET", INTEGRATIONS, json=jsonapi_collection([]))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool("prowler_list_integrations", {})
+
+ params = mock_router.query_params("GET", INTEGRATIONS)
+ assert "configuration" not in params["fields[integrations]"]
+ assert params["page[size]"] == "50"
+ assert params["page[number]"] == "1"
+
+
+async def test_listing_filters_by_type_with_a_comma_separated_value(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Multi-value filters reach the API as CSV, not as repeated query keys."""
+ mock_router.add(
+ "GET",
+ INTEGRATIONS,
+ json=jsonapi_collection(
+ [jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)]
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_list_integrations", {"integration_type": ["amazon_s3", "jira"]}
+ )
+
+ params = mock_router.query_params("GET", INTEGRATIONS)
+ assert params["filter[integration_type__in]"] == "amazon_s3,jira"
+ assert result.data["integrations"][0]["integration_type"] == "amazon_s3"
+
+
+async def test_getting_an_integration_returns_its_configuration(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The configuration is the whole reason this tool exists next to the list."""
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_get_integration", {"integration_id": "i1"}
+ )
+
+ assert result.data["configuration"]["projects"] == {"PROJ": "Security"}
+
+
+@pytest.mark.parametrize(
+ ("document", "message"),
+ [
+ ({"data": None}, "was not found"),
+ ({"data": {"type": "integrations", "id": "i1"}}, "without its attributes"),
+ ],
+ ids=["no-resource", "no-attributes"],
+)
+async def test_an_unusable_integration_payload_is_rejected_with_a_next_step(
+ mcp_root_server, mock_api_client, mock_router, document, message
+):
+ """Both shapes arrive as a 200, so neither raises on its own.
+
+ Left alone they surface as an opaque attribute error somewhere downstream
+ instead of telling the agent to go look the ID up. The two are reported
+ differently because a missing resource is the caller's mistake and a resource
+ without attributes is the API's.
+ """
+ mock_router.add("GET", INTEGRATION, json=document)
+
+ async with Client(mcp_root_server) as client:
+ with pytest.raises(Exception, match=message):
+ await client.call_tool("prowler_get_integration", {"integration_id": "i1"})
+
+
+# ---------------------------------------------------------------- create tools
+
+
+async def test_creating_an_s3_integration_checks_the_connection_before_returning(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Creation is a write, a connection check, a task poll and a re-read.
+
+ The check is not optional: it is what proves the bucket policy lets Prowler
+ write, and skipping it would report a broken integration as ready.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ json=jsonapi_document(jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)),
+ )
+ stub_connection_check(mock_router)
+ stub_integration(mock_router, S3_ATTRIBUTES, provider_ids=("p1",))
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_create_amazon_s3_integration",
+ {"bucket_name": "my-reports", "provider_ids": ["p1"]},
+ )
+
+ assert result.data["connected"] == "connected"
+ assert result.data["integration"]["id"] == "i1"
+ assert mock_router.paths() == [
+ f"POST {INTEGRATIONS}",
+ f"POST {CONNECTION}",
+ f"GET {TASK}",
+ f"GET {INTEGRATION}",
+ ]
+
+
+async def test_creating_an_s3_integration_sends_only_the_credentials_given(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Omitted credentials must be absent, not present and null.
+
+ An empty credentials object is a meaningful instruction -- use the ambient
+ AWS credentials of the deployment -- so sending nulls for the keys the caller
+ left out would be rejected instead of falling back.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ json=jsonapi_document(jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)),
+ )
+ stub_connection_check(mock_router)
+ stub_integration(mock_router, S3_ATTRIBUTES, provider_ids=("p1",))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_create_amazon_s3_integration",
+ {
+ "bucket_name": "my-reports",
+ "provider_ids": ["p1"],
+ "role_arn": "arn:aws:iam::123456789012:role/ProwlerS3Integration",
+ },
+ )
+
+ data = mock_router.json_body("POST", INTEGRATIONS)["data"]
+ assert data["attributes"]["credentials"] == {
+ "role_arn": "arn:aws:iam::123456789012:role/ProwlerS3Integration",
+ "session_duration": 3600,
+ }
+ assert data["attributes"]["configuration"] == {
+ "bucket_name": "my-reports",
+ "output_directory": "output",
+ }
+ assert data["relationships"]["providers"]["data"] == [
+ {"type": "providers", "id": "p1"}
+ ]
+
+
+async def test_creating_a_jira_integration_reduces_a_site_url_to_its_name(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The API only accepts the bare site name, and a URL is what people paste.
+
+ It also rejects any configuration in the payload, since it generates it from
+ the connection check.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ json=jsonapi_document(jsonapi_resource("integrations", "i1", JIRA_ATTRIBUTES)),
+ )
+ stub_connection_check(mock_router)
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_create_jira_integration",
+ {
+ "domain": "https://acme.atlassian.net/jira/software",
+ "user_mail": "security@acme.com",
+ "api_token": "fake-atlassian-token-for-testing",
+ },
+ )
+
+ attributes = mock_router.json_body("POST", INTEGRATIONS)["data"]["attributes"]
+ assert attributes["credentials"]["domain"] == "acme"
+ assert attributes["configuration"] == {}
+
+
+async def test_creating_a_jira_integration_rejects_an_empty_domain(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A domain that normalizes to nothing is caught before the round trip."""
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_create_jira_integration",
+ {
+ "domain": "https://",
+ "user_mail": "security@acme.com",
+ "api_token": "fake-atlassian-token-for-testing",
+ },
+ )
+
+ assert result.data["status"] == "failed"
+ assert "Invalid Jira domain" in result.data["error"]
+ assert mock_router.requests == []
+
+
+@pytest.mark.parametrize(
+ ("tool", "arguments"),
+ [
+ ("prowler_create_aws_security_hub_integration", {"provider_id": "p1"}),
+ ("prowler_create_amazon_s3_integration", {"bucket_name": "my-reports"}),
+ ],
+ ids=["security-hub", "amazon-s3"],
+)
+async def test_a_rejected_creation_is_reported_rather_than_raised(
+ mcp_root_server, mock_api_client, mock_router, tool, arguments
+):
+ """Write tools answer with an error object so the agent can act on it.
+
+ A raised exception reaches the model as a tool failure with no detail, and
+ the API's message is exactly what tells it what to do next.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ status=409,
+ json=jsonapi_error(409, "This provider already has this integration."),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(tool, arguments)
+
+ assert result.data["status"] == "failed"
+ assert "already has this integration" in result.data["error"]
+
+
+async def test_a_creation_with_no_id_back_warns_before_a_blind_retry(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The integration may well exist, so retrying could create a second one.
+
+ Without the ID there is nothing to check its connection with either, which
+ makes "look it up before trying again" the only safe instruction.
+ """
+ mock_router.add("POST", INTEGRATIONS, json={"data": {}})
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_create_amazon_s3_integration", {"bucket_name": "my-reports"}
+ )
+
+ assert result.data["status"] == "failed"
+ assert "did not return its ID" in result.data["error"]
+ assert mock_router.paths() == [f"POST {INTEGRATIONS}"]
+
+
+async def test_a_creation_whose_read_back_fails_still_hands_over_the_id(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The integration was created; only reading it back went wrong.
+
+ Reporting the read failure alone would read as "creation failed" and invite a
+ duplicate, so the error carries the ID the agent needs to go and inspect it.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ json=jsonapi_document(jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)),
+ )
+ stub_connection_check(mock_router)
+ mock_router.add(
+ "GET", INTEGRATION, status=500, json=jsonapi_error(500, "Server error.")
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_create_amazon_s3_integration", {"bucket_name": "my-reports"}
+ )
+
+ assert result.data["status"] == "failed"
+ assert "Integration i1 was created" in result.data["error"]
+
+
+async def test_a_connection_check_that_cannot_run_is_not_reported_as_a_failure(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """`not_tested` says nothing about the credentials, and that is the point.
+
+ Reporting it as `failed` would send an agent rewriting credentials that were
+ never actually exercised.
+ """
+ mock_router.add(
+ "POST",
+ INTEGRATIONS,
+ json=jsonapi_document(jsonapi_resource("integrations", "i1", S3_ATTRIBUTES)),
+ )
+ # Accepted, but without the task ID there is nothing to poll
+ mock_router.add("POST", CONNECTION, json={"data": {}})
+ stub_integration(mock_router, S3_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_create_amazon_s3_integration", {"bucket_name": "my-reports"}
+ )
+
+ assert result.data["connected"] == "not_tested"
+ assert "could not be completed" in result.data["error"]
+
+
+# ---------------------------------------------------------------- update tool
+
+
+async def test_updating_only_the_enabled_flag_does_not_recheck_the_connection(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Nothing about reachability changed, so the check would be pure latency.
+
+ It is also destructive to spend: the check is a background task the tool
+ waits on for up to two minutes. Skipping the check must not also skip the
+ read-back, though: the PATCH response body is empty, so returning the state
+ read before the write would report the integration as still enabled.
+ """
+ # The read before the PATCH, then the read after it
+ stub_integration(mock_router, S3_ATTRIBUTES)
+ stub_integration(mock_router, {**S3_ATTRIBUTES, "enabled": False})
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration", {"integration_id": "i1", "enabled": False}
+ )
+
+ assert mock_router.json_body("PATCH", INTEGRATION)["data"]["attributes"] == {
+ "enabled": False
+ }
+ assert f"POST {CONNECTION}" not in mock_router.paths()
+ assert result.data["enabled"] is False
+
+
+async def test_updating_the_configuration_merges_it_onto_the_current_one(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The API replaces the configuration wholesale, so a partial one loses keys.
+
+ The server-owned regions are stripped back out: they are refreshed by the
+ connection check, and sending them back would fight the API for ownership.
+ """
+ # The read before the PATCH, then the read after it
+ stub_integration(mock_router, SECURITY_HUB_ATTRIBUTES, provider_ids=("p1",))
+ stub_integration(
+ mock_router,
+ {
+ **SECURITY_HUB_ATTRIBUTES,
+ "configuration": {
+ **SECURITY_HUB_ATTRIBUTES["configuration"],
+ "send_only_fails": True,
+ },
+ },
+ provider_ids=("p1",),
+ )
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+ stub_connection_check(mock_router)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "configuration": {"send_only_fails": True}},
+ )
+
+ assert mock_router.json_body("PATCH", INTEGRATION)["data"]["attributes"][
+ "configuration"
+ ] == {"send_only_fails": True, "archive_previous_findings": True}
+ assert result.data["connected"] == "connected"
+ # Read back after the write, so the response carries the merge the API applied
+ assert result.data["integration"]["configuration"]["send_only_fails"] is True
+
+
+async def test_a_configuration_sent_as_a_json_string_is_accepted(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Some MCP clients cannot pass an object and send its JSON text instead.
+
+ Rejecting those outright would make the tool unusable from those clients,
+ which is why the parameter is typed to accept both.
+ """
+ stub_integration(mock_router, S3_ATTRIBUTES)
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+ stub_connection_check(mock_router)
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "configuration": '{"bucket_name": "new-reports"}'},
+ )
+
+ assert (
+ mock_router.json_body("PATCH", INTEGRATION)["data"]["attributes"][
+ "configuration"
+ ]["bucket_name"]
+ == "new-reports"
+ )
+
+
+@pytest.mark.parametrize(
+ ("configuration", "message"),
+ [
+ ("bucket_name=new", "Invalid JSON for configuration"),
+ ('["bucket_name"]', "configuration must be a JSON object"),
+ ],
+ ids=["not-json", "json-but-not-an-object"],
+)
+async def test_a_configuration_that_is_not_an_object_is_rejected_before_the_write(
+ mcp_root_server, mock_api_client, mock_router, configuration, message
+):
+ """Half-parsed text must not reach the API as a replacement configuration.
+
+ Valid JSON is not enough: the configuration is merged key by key, so a list
+ or a bare scalar would fail somewhere less obvious than here.
+ """
+ stub_integration(mock_router, S3_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "configuration": configuration},
+ )
+
+ assert result.data["status"] == "failed"
+ assert message in result.data["error"]
+ assert f"PATCH {INTEGRATION}" not in mock_router.paths()
+
+
+async def test_clearing_aws_credentials_is_allowed_and_means_something(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """An empty object is a valid instruction for the AWS integration types.
+
+ It falls back to the ambient credentials of the deployment, which is why the
+ Jira guard against an empty object must not apply here.
+ """
+ stub_integration(mock_router, S3_ATTRIBUTES)
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+ stub_connection_check(mock_router)
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "credentials": {}},
+ )
+
+ assert (
+ mock_router.json_body("PATCH", INTEGRATION)["data"]["attributes"]["credentials"]
+ == {}
+ )
+
+
+async def test_reordering_the_same_providers_does_not_recheck_the_connection(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The providers decide the effective credentials, so a real change matters.
+
+ Comparing them as sets keeps a re-sent list from paying for a two-minute
+ connection check that can only confirm what is already known.
+ """
+ stub_integration(mock_router, S3_ATTRIBUTES, provider_ids=("p1", "p2"))
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "provider_ids": ["p2", "p1"]},
+ )
+
+ assert f"POST {CONNECTION}" not in mock_router.paths()
+
+
+async def test_attaching_a_different_provider_rechecks_the_connection(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A different provider means different credentials and a stale check result."""
+ stub_integration(mock_router, S3_ATTRIBUTES, provider_ids=("p1",))
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+ stub_connection_check(mock_router, connected=False, error="Access denied.")
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "provider_ids": ["p2"]},
+ )
+
+ assert mock_router.json_body("PATCH", INTEGRATION)["data"]["relationships"] == {
+ "providers": {"data": [{"type": "providers", "id": "p2"}]}
+ }
+ assert result.data["connected"] == "failed"
+ assert result.data["error"] == "Access denied."
+
+
+async def test_an_update_with_nothing_to_change_returns_the_current_state(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """An empty PATCH would still cost a write and a connection check."""
+ stub_integration(mock_router, S3_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration", {"integration_id": "i1"}
+ )
+
+ assert result.data["id"] == "i1"
+ assert mock_router.paths() == [f"GET {INTEGRATION}"]
+
+
+async def test_updating_a_jira_configuration_is_refused(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Prowler generates the Jira configuration from the connection check.
+
+ Sending one would overwrite the discovered projects and issue types, leaving
+ an integration that looks fine but can no longer dispatch a finding.
+ """
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "configuration": {"domain": "other"}},
+ )
+
+ assert result.data["status"] == "failed"
+ assert "do not accept a configuration" in result.data["error"]
+ assert f"PATCH {INTEGRATION}" not in mock_router.paths()
+
+
+async def test_attaching_a_jira_integration_to_a_provider_is_refused(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Jira is tenant-wide; the API would reject this after a wasted round trip."""
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "provider_ids": ["p1"]},
+ )
+
+ assert "tenant-wide" in result.data["error"]
+ assert f"PATCH {INTEGRATION}" not in mock_router.paths()
+
+
+@pytest.mark.parametrize(
+ "provider_ids", [[], ["p1", "p2"]], ids=["detach-all", "two-providers"]
+)
+async def test_security_hub_must_keep_exactly_one_provider(
+ mcp_root_server, mock_api_client, mock_router, provider_ids
+):
+ """The integration cannot exist without its provider.
+
+ Detaching it through an update leaves the API to decide what that means; the
+ supported way to stop sending findings is to delete the integration.
+ """
+ stub_integration(mock_router, SECURITY_HUB_ATTRIBUTES, provider_ids=("p1",))
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "provider_ids": provider_ids},
+ )
+
+ assert "exactly one AWS provider" in result.data["error"]
+ assert f"PATCH {INTEGRATION}" not in mock_router.paths()
+
+
+@pytest.mark.parametrize(
+ "credentials",
+ [{}, {"domain": "acme"}, {"domain": "acme", "user_mail": "", "api_token": "t"}],
+ ids=["empty", "partial", "blank-value"],
+)
+async def test_partial_jira_credentials_are_refused_to_protect_the_stored_ones(
+ mcp_root_server, mock_api_client, mock_router, credentials
+):
+ """The API replaces the credentials object as a whole.
+
+ So a partial update does not patch the secret, it destroys it -- and the
+ integration cannot be repaired without the original API token.
+ """
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_update_integration",
+ {"integration_id": "i1", "credentials": credentials},
+ )
+
+ assert "replaced as a whole" in result.data["error"]
+ assert f"PATCH {INTEGRATION}" not in mock_router.paths()
+
+
+async def test_replacing_jira_credentials_normalizes_the_domain(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The same URL-to-site-name reduction the creation tool applies.
+
+ Without it a credentials replacement would store a domain the API cannot use,
+ breaking an integration that was working.
+ """
+ stub_integration(mock_router, JIRA_ATTRIBUTES)
+ mock_router.add("PATCH", INTEGRATION, json=jsonapi_document({}))
+ stub_connection_check(mock_router)
+
+ async with Client(mcp_root_server) as client:
+ await client.call_tool(
+ "prowler_update_integration",
+ {
+ "integration_id": "i1",
+ "credentials": {
+ "domain": "https://acme.atlassian.net",
+ "user_mail": "security@acme.com",
+ "api_token": "fake-atlassian-token-for-testing",
+ },
+ },
+ )
+
+ credentials = mock_router.json_body("PATCH", INTEGRATION)["data"]["attributes"][
+ "credentials"
+ ]
+ assert credentials["domain"] == "acme"
+
+
+# ------------------------------------------------- delete and connection tools
+
+
+async def test_deleting_an_integration_reports_the_outcome_either_way(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Deletion is irreversible, so both outcomes are stated explicitly.
+
+ A bare exception would leave the agent unsure whether the credentials are
+ gone, and a retry of a delete that actually succeeded reads as a new failure.
+ """
+ mock_router.add("DELETE", INTEGRATION, status=204)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_delete_integration", {"integration_id": "i1"}
+ )
+
+ assert result.data["deleted"] is True
+
+
+async def test_a_failed_deletion_says_it_did_not_happen(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """`deleted: false` is the part the agent must not have to infer."""
+ mock_router.add(
+ "DELETE", INTEGRATION, status=403, json=jsonapi_error(403, "Permission denied.")
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_delete_integration", {"integration_id": "i1"}
+ )
+
+ assert result.data["deleted"] is False
+ assert "Permission denied." in result.data["message"]
+
+
+async def test_checking_a_connection_surfaces_why_it_failed(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The error is the actionable half of a failed check."""
+ stub_connection_check(
+ mock_router, connected=False, error="Integration is not enabled"
+ )
+ stub_integration(
+ mock_router, {**S3_ATTRIBUTES, "enabled": False, "connected": False}
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_test_integration_connection", {"integration_id": "i1"}
+ )
+
+ assert result.data["connected"] == "failed"
+ assert result.data["error"] == "Integration is not enabled"
+ # The re-read is what makes the refreshed configuration part of the answer
+ assert mock_router.paths() == [
+ f"POST {CONNECTION}",
+ f"GET {TASK}",
+ f"GET {INTEGRATION}",
+ ]
+
+
+# ------------------------------------------------------------------ jira tools
+
+
+async def test_issue_types_are_requested_for_a_specific_project(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Issue types differ per project, so the key has to reach the API."""
+ mock_router.add(
+ "GET",
+ ISSUE_TYPES,
+ json={"data": {"project_key": "PROJ", "issue_types": ["Task", "Bug"]}},
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_get_jira_issue_types",
+ {"integration_id": "i1", "project_key": "PROJ"},
+ )
+
+ assert result.data["issue_types"] == ["Task", "Bug"]
+ assert mock_router.query_params("GET", ISSUE_TYPES)["project_key"] == "PROJ"
+
+
+async def test_dispatching_findings_sends_them_as_a_filter_not_a_body_field(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """The findings are selected by query filter; the body carries the target.
+
+ Putting the IDs in the wrong half of the request is not an error the API
+ reports -- it dispatches a different, unfiltered set of findings.
+ """
+ mock_router.add(
+ "POST", DISPATCHES, json=jsonapi_document(jsonapi_resource("tasks", "t1", {}))
+ )
+ mock_router.add(
+ "GET",
+ TASK,
+ json=jsonapi_document(
+ jsonapi_resource(
+ "tasks",
+ "t1",
+ {
+ "state": "completed",
+ "result": {"created_count": 2, "failed_count": 0},
+ },
+ )
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1", "f2"],
+ },
+ )
+
+ assert mock_router.query_params("POST", DISPATCHES)["filter[finding_id__in]"] == (
+ "f1,f2"
+ )
+ assert mock_router.json_body("POST", DISPATCHES)["data"]["attributes"] == {
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ }
+ assert result.data["created_count"] == 2
+ assert result.data["safe_to_retry"] is False
+
+
+async def test_dispatching_no_findings_is_refused_before_the_request(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """An empty filter would dispatch every finding the role can see."""
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": [],
+ },
+ )
+
+ assert result.data["status"] == "failed"
+ assert result.data["safe_to_retry"] is True
+ assert mock_router.requests == []
+
+
+@pytest.mark.parametrize(
+ "status", [400, 403, 404], ids=["invalid", "forbidden", "not-found"]
+)
+async def test_a_dispatch_the_api_refused_is_the_only_one_safe_to_retry(
+ mcp_root_server, mock_api_client, mock_router, status
+):
+ """A client error is a refusal: the API rejects the dispatch before queueing it.
+
+ That makes it the one dispatch failure an agent can act on directly, so the
+ response has to say so -- an omitted `safe_to_retry` reads as "do not retry"
+ and leaves fixing the issue type to a human.
+ """
+ mock_router.add(
+ "POST",
+ DISPATCHES,
+ status=status,
+ json=jsonapi_error(
+ status, "Issue type 'Epic' requires fields Prowler cannot fill."
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Epic",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "failed"
+ assert result.data["safe_to_retry"] is True
+ assert "requires fields Prowler cannot fill" in result.data["error"]
+
+
+async def test_a_dispatch_that_failed_on_the_server_is_not_safe_to_retry(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A server error is not a refusal, and this is where that distinction bites.
+
+ The API queues the background task and only then serializes its answer, so a
+ 500 can come back with work items already being created. Treating every error
+ status as a clean rejection would invite a resend on top of them.
+ """
+ mock_router.add(
+ "POST", DISPATCHES, status=500, json=jsonapi_error(500, "Server error.")
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "unknown"
+ assert result.data["safe_to_retry"] is False
+ assert "Check the Jira project" in result.data["error"]
+
+
+async def test_a_dispatch_request_that_got_no_answer_is_not_safe_to_retry(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """A timeout is not a rejection either: the request may have been processed.
+
+ Prowler could already be creating work items, so the only difference with a
+ refused dispatch -- and the reason they cannot share a branch -- is that here
+ nobody can say what was created.
+ """
+
+ def timed_out(request):
+ raise httpx.ReadTimeout("Timed out reading the response", request=request)
+
+ mock_router.add_handler("POST", DISPATCHES, timed_out)
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "unknown"
+ assert result.data["safe_to_retry"] is False
+ assert "Check the Jira project" in result.data["error"]
+
+
+async def test_an_accepted_dispatch_with_no_task_id_is_not_safe_to_retry(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Prowler took the dispatch, so it is already creating work items.
+
+ There is just no task to follow it with. Reporting that as a clean failure
+ would invite a resend on top of whatever it created.
+ """
+ mock_router.add("POST", DISPATCHES, json={"data": {}})
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "unknown"
+ assert result.data["safe_to_retry"] is False
+ assert "task_id" not in result.data
+
+
+async def test_a_dispatch_task_that_died_halfway_is_never_safe_to_retry(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Work items are created one at a time and Prowler cannot delete them.
+
+ A task that failed may have created any number of them first, so the honest
+ answer is `unknown` plus a pointer at Jira -- never an invitation to resend.
+ """
+ mock_router.add(
+ "POST", DISPATCHES, json=jsonapi_document(jsonapi_resource("tasks", "t1", {}))
+ )
+ mock_router.add(
+ "GET",
+ TASK,
+ json=jsonapi_document(
+ jsonapi_resource("tasks", "t1", {"state": "failed", "error": "Jira 503"})
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "unknown"
+ assert result.data["safe_to_retry"] is False
+ assert result.data["task_id"] == "t1"
+
+
+async def test_a_dispatch_still_running_when_the_wait_ends_reports_in_progress(
+ mcp_root_server, mock_api_client, mock_router, monkeypatch
+):
+ """Giving up waiting is not the same as the dispatch stopping.
+
+ It is still creating work items right now, so the response has to say so and
+ hand back the task ID rather than let the agent conclude nothing happened.
+ """
+ from prowler_mcp_server.prowler_app.tools import integrations
+
+ monkeypatch.setattr(integrations, "JIRA_DISPATCH_TIMEOUT", 0)
+ mock_router.add(
+ "POST", DISPATCHES, json=jsonapi_document(jsonapi_resource("tasks", "t1", {}))
+ )
+ mock_router.add(
+ "GET",
+ TASK,
+ json=jsonapi_document(jsonapi_resource("tasks", "t1", {"state": "executing"})),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "in_progress"
+ assert result.data["safe_to_retry"] is False
+ assert result.data["task_id"] == "t1"
+
+
+async def test_a_completed_dispatch_with_no_counters_is_reported_as_unknown(
+ mcp_root_server, mock_api_client, mock_router
+):
+ """Absent counters must not be read as zero.
+
+ Zero created work items is precisely what makes a dispatch safe to retry, so
+ defaulting them would invite the duplication this whole path exists to avoid.
+ """
+ mock_router.add(
+ "POST", DISPATCHES, json=jsonapi_document(jsonapi_resource("tasks", "t1", {}))
+ )
+ mock_router.add(
+ "GET",
+ TASK,
+ json=jsonapi_document(
+ jsonapi_resource("tasks", "t1", {"state": "completed", "result": None})
+ ),
+ )
+
+ async with Client(mcp_root_server) as client:
+ result = await client.call_tool(
+ "prowler_send_findings_to_jira",
+ {
+ "integration_id": "i1",
+ "project_key": "PROJ",
+ "issue_type": "Task",
+ "finding_ids": ["f1"],
+ },
+ )
+
+ assert result.data["status"] == "unknown"
+ assert result.data["safe_to_retry"] is False
diff --git a/mcp_server/tests/prowler_app/utils/__init__.py b/mcp_server/tests/prowler_app/utils/__init__.py
new file mode 100644
index 0000000000..9c63f3cf09
--- /dev/null
+++ b/mcp_server/tests/prowler_app/utils/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler App shared utilities."""
diff --git a/mcp_server/tests/prowler_app/utils/test_api_client.py b/mcp_server/tests/prowler_app/utils/test_api_client.py
new file mode 100644
index 0000000000..41aceedcab
--- /dev/null
+++ b/mcp_server/tests/prowler_app/utils/test_api_client.py
@@ -0,0 +1,108 @@
+"""Tests for the shared Prowler API client.
+
+Reference for later branches: drive the client through ``mock_api_client`` +
+``mock_router`` and assert on the recorded request, so the real URL joining,
+query encoding and header assembly stay covered.
+"""
+
+import httpx
+import pytest
+
+from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIError
+from tests.helpers.jsonapi import jsonapi_collection, jsonapi_error, jsonapi_resource
+from tests.helpers.tokens import FAKE_API_KEY
+
+
+async def test_get_sends_an_authenticated_jsonapi_request(mock_api_client, mock_router):
+ """A GET carries the API key and the JSON:API content negotiation headers."""
+ mock_router.add(
+ "GET",
+ "/api/v1/findings",
+ json=jsonapi_collection(
+ [jsonapi_resource("findings", "f1", {"severity": "high"})]
+ ),
+ )
+
+ await mock_api_client.get("/findings")
+
+ request = mock_router.request_for("GET", "/api/v1/findings")
+ assert request.headers["authorization"] == f"Api-Key {FAKE_API_KEY}"
+ assert request.headers["accept"] == "application/vnd.api+json"
+ assert request.headers["user-agent"].startswith("prowler-mcp-server/")
+
+
+async def test_get_forwards_query_parameters(mock_api_client, mock_router):
+ """Filter parameters reach the wire with their JSON:API bracket syntax intact."""
+ mock_router.add("GET", "/api/v1/findings", json=jsonapi_collection([]))
+
+ await mock_api_client.get(
+ "/findings", params={"page[size]": 5, "filter[severity__in]": "critical"}
+ )
+
+ assert mock_router.query_params("GET", "/api/v1/findings") == {
+ "page[size]": "5",
+ "filter[severity__in]": "critical",
+ }
+
+
+async def test_error_response_surfaces_the_jsonapi_detail(mock_api_client, mock_router):
+ """A failed request is raised with the API's own `errors[].detail` message.
+
+ Tools relay this text straight to the model, so losing it turns an actionable
+ error into an opaque one.
+ """
+ mock_router.add(
+ "GET",
+ "/api/v1/findings/nope",
+ status=404,
+ json=jsonapi_error(404, "Not found."),
+ )
+
+ with pytest.raises(
+ ProwlerAPIError, match=r"API request failed: 404 - Not found\."
+ ) as raised:
+ await mock_api_client.get("/findings/nope")
+
+ assert raised.value.status_code == 404
+
+
+async def test_a_request_that_got_no_answer_is_not_an_api_error(
+ mock_api_client, mock_router
+):
+ """`ProwlerAPIError` means the API answered, and callers act on that.
+
+ A write tool tells a rejected request -- which changed nothing -- from one
+ that may have been processed by the type of the failure, so a timeout must
+ not be dressed up as a rejection.
+ """
+
+ def timed_out(request):
+ raise httpx.ReadTimeout("Timed out reading the response", request=request)
+
+ mock_router.add_handler("GET", "/api/v1/findings", timed_out)
+
+ with pytest.raises(httpx.ReadTimeout):
+ await mock_api_client.get("/findings")
+
+
+def test_build_filter_params_normalises_types_for_the_api(mock_api_client):
+ """Booleans become lowercase strings, sequences become CSV, `None` is dropped."""
+ result = mock_api_client.build_filter_params(
+ {
+ "filter[muted]": True,
+ "filter[severity__in]": ["high", "critical"],
+ "filter[status]": None,
+ "page[size]": 50,
+ }
+ )
+
+ assert result == {
+ "filter[muted]": "true",
+ "filter[severity__in]": "high,critical",
+ "page[size]": 50,
+ }
+
+
+def test_the_api_client_is_a_singleton(isolated_api_client):
+ """Every tool must share one client so the HTTP connection pool is shared."""
+ assert isolated_api_client() is isolated_api_client()
diff --git a/mcp_server/tests/prowler_app/utils/test_auth.py b/mcp_server/tests/prowler_app/utils/test_auth.py
new file mode 100644
index 0000000000..d39e5826d7
--- /dev/null
+++ b/mcp_server/tests/prowler_app/utils/test_auth.py
@@ -0,0 +1,62 @@
+"""Tests for Prowler API authentication.
+
+Reference for later branches: ``ProwlerAppAuth`` resolves its ``mode`` and
+``base_url`` in default arguments, which Python evaluates once at module import.
+``monkeypatch.setenv`` therefore has no effect on them -- always pass ``mode=``
+and ``base_url=`` explicitly, as these tests do.
+"""
+
+import pytest
+
+from prowler_mcp_server.prowler_app.utils.auth import ProwlerAppAuth
+from tests.helpers.tokens import FAKE_API_KEY, MALFORMED_API_KEY, fake_jwt
+
+
+async def test_stdio_mode_reads_the_api_key_from_the_environment():
+ """In STDIO transport the key comes from the process environment."""
+ auth = ProwlerAppAuth(mode="stdio")
+
+ assert await auth.get_valid_token() == FAKE_API_KEY
+
+
+def test_stdio_mode_rejects_a_key_without_the_prowler_prefix(
+ monkeypatch: pytest.MonkeyPatch,
+):
+ """A key that is not `pk_`-prefixed is refused at construction.
+
+ Failing here rather than on the first API call is what turns a
+ misconfiguration into an immediate, readable startup error.
+ """
+ monkeypatch.setenv("PROWLER_API_KEY", MALFORMED_API_KEY)
+
+ with pytest.raises(ValueError, match="Prowler API key format is incorrect"):
+ ProwlerAppAuth(mode="stdio")
+
+
+async def test_http_mode_accepts_a_bearer_api_key(http_request_headers):
+ """In HTTP transport the token comes from the request's Authorization header."""
+ http_request_headers(authorization=f"Bearer {FAKE_API_KEY}")
+
+ auth = ProwlerAppAuth(mode="http")
+
+ assert await auth.get_valid_token() == FAKE_API_KEY
+
+
+async def test_http_mode_rejects_an_expired_jwt(http_request_headers):
+ """An expired JWT is refused locally instead of being forwarded to the API."""
+ http_request_headers(authorization=f"Bearer {fake_jwt(expires_in=-60)}")
+
+ auth = ProwlerAppAuth(mode="http")
+
+ with pytest.raises(ValueError, match="Token has expired"):
+ await auth.get_valid_token()
+
+
+def test_api_keys_and_jwts_use_different_authorization_schemes():
+ """Prowler API keys authenticate with `Api-Key`, JWTs with `Bearer`."""
+ auth = ProwlerAppAuth(mode="stdio")
+
+ assert auth.get_headers(FAKE_API_KEY)["Authorization"] == f"Api-Key {FAKE_API_KEY}"
+
+ jwt = fake_jwt()
+ assert auth.get_headers(jwt)["Authorization"] == f"Bearer {jwt}"
diff --git a/mcp_server/tests/prowler_documentation/__init__.py b/mcp_server/tests/prowler_documentation/__init__.py
new file mode 100644
index 0000000000..982ef891e6
--- /dev/null
+++ b/mcp_server/tests/prowler_documentation/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler Documentation sub-server."""
diff --git a/mcp_server/tests/prowler_hub/__init__.py b/mcp_server/tests/prowler_hub/__init__.py
new file mode 100644
index 0000000000..9c5c9f6cd2
--- /dev/null
+++ b/mcp_server/tests/prowler_hub/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Prowler Hub sub-server."""
diff --git a/mcp_server/tests/test_health.py b/mcp_server/tests/test_health.py
index 47a676960d..f52b9a93e0 100644
--- a/mcp_server/tests/test_health.py
+++ b/mcp_server/tests/test_health.py
@@ -1,16 +1,11 @@
"""Tests for the Prowler MCP Server health endpoint."""
-from starlette.testclient import TestClient
-
from prowler_mcp_server import __version__
-from prowler_mcp_server.server import app
-def test_health_returns_ietf_pass_response():
+def test_health_returns_ietf_pass_response(health_client):
"""GET /health returns 200 with the IETF health-check body and headers."""
- client = TestClient(app)
-
- response = client.get("/health")
+ response = health_client.get("/health")
assert response.status_code == 200
assert response.headers["content-type"] == "application/health+json"
@@ -24,23 +19,19 @@ def test_health_returns_ietf_pass_response():
}
-def test_health_release_id_matches_package_version():
+def test_health_release_id_matches_package_version(health_client):
"""The endpoint must surface the current package __version__ as releaseId.
Drift between the response and the installed package would mislead any
monitoring tool that uses releaseId to identify the running build.
"""
- client = TestClient(app)
-
- response = client.get("/health")
+ response = health_client.get("/health")
assert response.json()["releaseId"] == __version__
-def test_health_rejects_non_get_methods():
+def test_health_rejects_non_get_methods(health_client):
"""The endpoint only exposes GET; other verbs return 405."""
- client = TestClient(app)
-
- response = client.post("/health")
+ response = health_client.post("/health")
assert response.status_code == 405
diff --git a/mcp_server/tests/test_server.py b/mcp_server/tests/test_server.py
new file mode 100644
index 0000000000..30ab1fc92c
--- /dev/null
+++ b/mcp_server/tests/test_server.py
@@ -0,0 +1,51 @@
+"""Tests for the mounted root MCP server.
+
+Reference for later branches: open the client inline with
+``async with Client(mcp_root_server)``. FastMCP warns against holding a client in
+a fixture because it causes hard-to-diagnose event-loop problems.
+"""
+
+from fastmcp import Client
+
+from tests.helpers.assertions import (
+ assert_namespaced,
+ assert_tool_contract,
+ tools_in_namespace,
+)
+
+
+async def test_every_sub_server_contributes_tools(mcp_root_server):
+ """Each of the three mounts must expose tools under its own namespace.
+
+ This is the guard against a silent startup failure. ``setup_main_server()``
+ wraps each mount in try/except and ``load_all_tools`` swallows per-tool
+ construction errors, so a sub-server that registers nothing is still logged as
+ "successfully mounted". The `prowler_*` namespace in particular collapses to
+ zero tools whenever the API key is missing when the module is first imported.
+ """
+ async with Client(mcp_root_server) as client:
+ tools = await client.list_tools()
+
+ assert tools_in_namespace(tools, "prowler_hub_"), "Prowler Hub registered no tools"
+ assert tools_in_namespace(tools, "prowler_docs_"), (
+ "Prowler Docs registered no tools"
+ )
+ assert tools_in_namespace(tools, "prowler_"), "Prowler App registered no tools"
+
+
+async def test_every_tool_is_namespaced(mcp_root_server):
+ """Tool names are a published interface; nothing may escape the namespaces."""
+ async with Client(mcp_root_server) as client:
+ tools = await client.list_tools()
+
+ for tool in tools:
+ assert_namespaced(tool)
+
+
+async def test_every_tool_and_parameter_is_described(mcp_root_server):
+ """Descriptions are the contract a model reads before calling a tool."""
+ async with Client(mcp_root_server) as client:
+ tools = await client.list_tools()
+
+ for tool in tools:
+ assert_tool_contract(tool)
diff --git a/mcp_server/uv.lock b/mcp_server/uv.lock
index 3258767442..849847e3c8 100644
--- a/mcp_server/uv.lock
+++ b/mcp_server/uv.lock
@@ -1,6 +1,20 @@
version = 1
revision = 3
requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform != 'win32'",
+]
+
+[manifest]
+constraints = [
+ { name = "cryptography", specifier = "==50.0.0" },
+ { name = "joserfc", specifier = "==1.6.8" },
+ { name = "mcp", specifier = "==1.28.1" },
+ { name = "python-multipart", specifier = "==0.0.30" },
+]
[[package]]
name = "aiofile"
@@ -199,57 +213,123 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
+[[package]]
+name = "coverage"
+version = "7.15.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" },
+ { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" },
+ { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" },
+ { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" },
+ { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" },
+ { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" },
+ { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" },
+ { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" },
+ { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" },
+ { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" },
+ { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" },
+ { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" },
+]
+
[[package]]
name = "cryptography"
-version = "48.0.0"
+version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
- { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
- { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
- { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
- { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
- { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
- { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
- { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
- { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
- { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
- { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
- { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
- { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
- { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
- { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
- { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
- { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
- { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
- { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
- { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
- { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
- { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
- { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
- { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
- { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
- { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
- { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
- { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
- { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
- { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
- { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
- { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
- { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
- { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
- { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
- { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
+ { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
+ { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
+ { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
+ { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
+ { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
+ { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
+ { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
+ { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
+ { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
+ { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
+ { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
+ { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
+ { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
]
[[package]]
@@ -321,36 +401,66 @@ wheels = [
[[package]]
name = "fastmcp"
-version = "3.2.4"
+version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "fastmcp-slim", extra = ["client", "server"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/23/14/c1ffb91b7d1fece86c81e1f9df5474f30fd97e4cdaa398814bbbeee88568/fastmcp-3.4.5.tar.gz", hash = "sha256:a95f2bc876bef42e8b50f7872f24f3f2fe3b1d37408c734e8b9d9e03014b72d3", size = 28800521, upload-time = "2026-07-27T19:20:01.231Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/4f/73450a436c963c0382d15a882fc5d08f15aadc329194df1b54495a7c8383/fastmcp-3.4.5-py3-none-any.whl", hash = "sha256:5d3d438eb2917e63e6faf53e8cb8fe26d887ec3232f848093a4eecad7fa34861", size = 8017, upload-time = "2026-07-27T19:19:57.942Z" },
+]
+
+[[package]]
+name = "fastmcp-slim"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "platformdirs" },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pydantic-settings" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/1d/f3e271fbcd01ce01a4cf623b336d8e1305c192aa5d5e8e0223b7167462e9/fastmcp_slim-3.4.5.tar.gz", hash = "sha256:5badc3bceee61f61297eeb9494f499325f3ce1cafabf4611b31f6c3e9d7dff59", size = 591622, upload-time = "2026-07-27T19:15:19.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/3b/16d8aa8224094519f30b078138e725b8a731bf0a13f1f850e58b5f9b3cc4/fastmcp_slim-3.4.5-py3-none-any.whl", hash = "sha256:bc31217827c4999812543c83ee95ed9a47f3ed1e3fd0bd4f64371e375b748eca", size = 766478, upload-time = "2026-07-27T19:15:18.015Z" },
+]
+
+[package.optional-dependencies]
+client = [
+ { name = "authlib" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "opentelemetry-api" },
+ { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
+ { name = "starlette" },
+]
+server = [
{ name = "authlib" },
{ name = "cyclopts" },
{ name = "exceptiongroup" },
{ name = "griffelib" },
{ name = "httpx" },
+ { name = "joserfc" },
{ name = "jsonref" },
{ name = "jsonschema-path" },
{ name = "mcp" },
{ name = "openapi-pydantic" },
{ name = "opentelemetry-api" },
{ name = "packaging" },
- { name = "platformdirs" },
{ name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
- { name = "pydantic", extra = ["email"] },
{ name = "pyperclip" },
- { name = "python-dotenv" },
+ { name = "python-multipart" },
{ name = "pyyaml" },
- { name = "rich" },
+ { name = "starlette" },
{ name = "uncalled-for" },
{ name = "uvicorn" },
{ name = "watchfiles" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" },
-]
[[package]]
name = "griffelib"
@@ -481,14 +591,14 @@ wheels = [
[[package]]
name = "joserfc"
-version = "1.6.5"
+version = "1.6.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5d/ac/d4fd5b30f82900eac60d765f179f0ba005825ac462cc8ced6e13ec685ab3/joserfc-1.6.8.tar.gz", hash = "sha256:878620c553a6ebdd76ccdc356782fee3f735f21a356d079a546b42a4670ace5f", size = 232930, upload-time = "2026-05-27T03:22:37.819Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" },
+ { url = "https://files.pythonhosted.org/packages/98/8c/5cdce2cf3ce8155849baf9a5e2ce77e89dc87ec3bdb38259e5d85fbc45bd/joserfc-1.6.8-py3-none-any.whl", hash = "sha256:22fb31a69094a5e6f44632002a9df2c30c941fc6c8ce1b037e92c03de954cf9f", size = 70927, upload-time = "2026-05-27T03:22:35.796Z" },
]
[[package]]
@@ -572,7 +682,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.27.1"
+version = "1.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -590,9 +700,9 @@ dependencies = [
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
]
[[package]]
@@ -676,7 +786,7 @@ wheels = [
[[package]]
name = "prowler-mcp"
-version = "0.5.0"
+version = "0.9.0"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
@@ -686,21 +796,29 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "bandit" },
+ { name = "coverage" },
{ name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+ { name = "pytest-env" },
{ name = "ruff" },
{ name = "vulture" },
]
[package.metadata]
requires-dist = [
- { name = "fastmcp", specifier = "==3.2.4" },
+ { name = "fastmcp", specifier = "==3.4.5" },
{ name = "httpx", specifier = "==0.28.1" },
]
[package.metadata.requires-dev]
dev = [
{ name = "bandit", specifier = "==1.8.3" },
+ { name = "coverage", specifier = "==7.15.2" },
{ name = "pytest", specifier = "==9.0.3" },
+ { name = "pytest-asyncio", specifier = "==1.4.0" },
+ { name = "pytest-cov", specifier = "==6.0.0" },
+ { name = "pytest-env", specifier = "==1.1.5" },
{ name = "ruff", specifier = "==0.15.11" },
{ name = "vulture", specifier = "==2.14" },
]
@@ -896,6 +1014,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945, upload-time = "2024-10-29T20:13:35.363Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949, upload-time = "2024-10-29T20:13:33.215Z" },
+]
+
+[[package]]
+name = "pytest-env"
+version = "1.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" },
+]
+
[[package]]
name = "python-dotenv"
version = "1.2.2"
@@ -907,11 +1063,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.28"
+version = "0.0.30"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
]
[[package]]
diff --git a/permissions/prowler-additions-policy.json b/permissions/prowler-additions-policy.json
index eb140e7c09..13aff68168 100644
--- a/permissions/prowler-additions-policy.json
+++ b/permissions/prowler-additions-policy.json
@@ -4,6 +4,8 @@
{
"Action": [
"account:Get*",
+ "amplify:ListApps",
+ "amplify:ListBranches",
"appstream:Describe*",
"appstream:List*",
"backup:List*",
@@ -15,6 +17,9 @@
"codebuild:BatchGet*",
"codebuild:ListReportGroups",
"cognito-idp:GetUserPoolMfaConfig",
+ "datapipeline:DescribePipelines",
+ "datapipeline:GetPipelineDefinition",
+ "datapipeline:ListPipelines",
"dlm:Get*",
"drs:Describe*",
"ds:Get*",
@@ -32,6 +37,7 @@
"glue:SearchTables",
"glue:GetMLTransforms",
"lambda:GetFunction*",
+ "lambda:GetLayerVersion",
"logs:FilterLogEvents",
"lightsail:GetRelationalDatabases",
"macie2:GetMacieSession",
diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml
index a7d91b0071..c9eee71950 100644
--- a/permissions/templates/cloudformation/prowler-scan-role.yml
+++ b/permissions/templates/cloudformation/prowler-scan-role.yml
@@ -1,27 +1,24 @@
AWSTemplateFormatVersion: "2010-09-09"
-# You can invoke CloudFormation and pass the principal ARN from a command line like this:
-# aws cloudformation create-stack \
-# --capabilities CAPABILITY_IAM --capabilities CAPABILITY_NAMED_IAM \
-# --template-body "file://prowler-scan-role.yaml" \
-# --stack-name "ProwlerScanRole" \
-# --parameters "ParameterKey=ExternalId,ParameterValue=ProvidedExternalID"
-
Description: |
- This template creates the ProwlerScan IAM Role in this account with
- all read-only permissions to scan your account for security issues.
+ This template creates the ProwlerScan IAM Role either locally in this account or across
+ multiple accounts via StackSets. It can deploy both simultaneously or just one option.
+ The role includes all read-only permissions to scan your accounts for security issues.
Contains two AWS managed policies (SecurityAudit and ViewOnlyAccess) and an inline policy.
- It sets the trust policy on that IAM Role to permit Prowler to assume that role.
This template is designed to be used in Prowler Cloud, but can also be used in other Prowler deployments.
+
If you are deploying this template to be used in Prowler Cloud please do not edit the AccountId, IAMPrincipal and ExternalId parameters.
+
Parameters:
+ # Core Prowler IAM Role Parameters
ExternalId:
Description: |
- This is the External ID that Prowler will use to assume the role ProwlerScan IAM Role.
+ This is the External ID that Prowler will use to assume the ProwlerScan IAM Role.
Type: String
MinLength: 1
AllowedPattern: ".+"
ConstraintDescription: "ExternalId must not be empty."
+
AccountId:
Description: |
AWS Account ID that will assume the role created, if you are deploying this template to be used in Prowler Cloud please do not edit this.
@@ -31,11 +28,13 @@ Parameters:
MaxLength: 12
AllowedPattern: "[0-9]{12}"
ConstraintDescription: "AccountId must be a valid AWS Account ID."
+
IAMPrincipal:
Description: |
The IAM principal type and name that will be allowed to assume the role created, leave an * for all the IAM principals in your AWS account. If you are deploying this template to be used in Prowler Cloud please do not edit this.
Type: String
Default: role/prowler*
+
EnableOrganizations:
Description: |
Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account.
@@ -45,6 +44,7 @@ Parameters:
AllowedValues:
- true
- false
+
EnableS3Integration:
Description: |
Enable S3 integration for storing Prowler scan reports.
@@ -53,25 +53,102 @@ Parameters:
AllowedValues:
- true
- false
+
S3IntegrationBucketName:
Description: |
The S3 bucket name where Prowler will store scan reports for your cloud providers.
Type: String
Default: ""
+
S3IntegrationBucketAccountId:
Description: |
The AWS Account ID owner of the S3 Bucket.
Type: String
Default: ""
+ # Deployment Control Parameters
+ DeployStackSet:
+ Description: |
+ Set to true to deploy the ProwlerScan role across multiple accounts using StackSets.
+ Requires delegated administrator permissions for CloudFormation StackSets.
+ Type: String
+ Default: false
+ AllowedValues:
+ - true
+ - false
+
+ DeployLocalRole:
+ Description: |
+ Set to true to deploy the ProwlerScan role in this account (the account where this template is deployed).
+ Can be used independently or in conjunction with StackSet deployment.
+ Type: String
+ Default: true
+ AllowedValues:
+ - true
+ - false
+
+ # StackSet Configuration Parameters
+ AWSOrganizationalUnitId:
+ Description: |
+ AWS Organizations OU to deploy this stackset to (e.g., ou-xxxx-yyyyyyyy or r-xxxx for root).
+ Only required if DeployStackSet is true.
+ Type: String
+ Default: ""
+ AllowedPattern: '^(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})?$'
+
+ RetainStacksOnAccountRemoval:
+ Description: |
+ When an account is removed from the Organization or OU, should the ProwlerScan role remain in that account?
+ False (Recommended for security): Automatically deletes the role when accounts leave, following principle of least privilege.
+ True: Retains the role even after account removal, useful if accounts may temporarily leave and rejoin.
+ Type: String
+ Default: false
+ AllowedValues:
+ - true
+ - false
+
+ DeployFromDelegatedAdmin:
+ Description: |
+ Is this StackSet being deployed from a Delegated Administrator account (not the Organization Management Account)?
+ True: Deploying from a delegated admin account - uses CallAs: DELEGATED_ADMIN.
+ False: Deploying from the Organization Management Account - omits CallAs property.
+ Only required if DeployStackSet is true.
+ Type: String
+ Default: false
+ AllowedValues:
+ - true
+ - false
+
+ FailureTolerancePercentage:
+ Description: |
+ The percentage of accounts in which stack operations can fail before CloudFormation stops the operation.
+ Only applies when DeployStackSet is true.
+ Type: Number
+ Default: 10
+ MinValue: 0
+ MaxValue: 100
+
Conditions:
OrganizationsEnabled: !Equals [!Ref EnableOrganizations, true]
S3IntegrationEnabled: !Equals [!Ref EnableS3Integration, true]
+ DeployStackSetEnabled: !Equals [!Ref DeployStackSet, true]
+ DeployLocalRoleEnabled: !Equals [!Ref DeployLocalRole, true]
+ UseDelegatedAdmin: !Equals [!Ref DeployFromDelegatedAdmin, true]
+Rules:
+ S3IntegrationRequiresParams:
+ RuleCondition: !Equals [!Ref EnableS3Integration, "true"]
+ Assertions:
+ - Assert: !Not [!Equals [!Ref S3IntegrationBucketName, ""]]
+ AssertDescription: "S3IntegrationBucketName is required when EnableS3Integration is true."
+ - Assert: !Not [!Equals [!Ref S3IntegrationBucketAccountId, ""]]
+ AssertDescription: "S3IntegrationBucketAccountId is required when EnableS3Integration is true."
Resources:
+ # Local ProwlerScan Role (deployed in this account)
ProwlerScan:
Type: AWS::IAM::Role
+ Condition: DeployLocalRoleEnabled
Properties:
RoleName: ProwlerScan
AssumeRolePolicyDocument:
@@ -88,8 +165,8 @@ Resources:
"aws:PrincipalArn": !Sub "arn:${AWS::Partition}:iam::${AccountId}:${IAMPrincipal}"
MaxSessionDuration: 3600
ManagedPolicyArns:
- - "arn:aws:iam::aws:policy/SecurityAudit"
- - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit"
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess"
Policies:
- PolicyName: ProwlerScan
PolicyDocument:
@@ -99,9 +176,12 @@ Resources:
Effect: Allow
Action:
- "account:Get*"
+ - "amplify:ListApps"
+ - "amplify:ListBranches"
- "appstream:Describe*"
- "appstream:List*"
- "backup:List*"
+ - "backup:Get*"
- "bedrock:List*"
- "bedrock:Get*"
- "cloudtrail:GetInsightSelectors"
@@ -109,6 +189,9 @@ Resources:
- "codebuild:BatchGet*"
- "codebuild:ListReportGroups"
- "cognito-idp:GetUserPoolMfaConfig"
+ - "datapipeline:DescribePipelines"
+ - "datapipeline:GetPipelineDefinition"
+ - "datapipeline:ListPipelines"
- "dlm:Get*"
- "drs:Describe*"
- "ds:Get*"
@@ -124,6 +207,7 @@ Resources:
- "glue:GetConnections"
- "glue:GetSecurityConfiguration*"
- "glue:SearchTables"
+ - "glue:GetMLTransforms"
- "lambda:GetFunction*"
- "logs:FilterLogEvents"
- "lightsail:GetRelationalDatabases"
@@ -134,7 +218,6 @@ Resources:
- "s3:GetAccountPublicAccessBlock"
- "shield:DescribeProtection"
- "shield:GetSubscriptionState"
- - "securityhub:BatchImportFindings"
- "securityhub:GetFindings"
- "servicecatalog:Describe*"
- "servicecatalog:List*"
@@ -145,15 +228,20 @@ Resources:
- "tag:GetTagKeys"
- "wellarchitected:List*"
Resource: "*"
+ - Sid: AllowSecurityHubImportFindings
+ Effect: Allow
+ Action:
+ - "securityhub:BatchImportFindings"
+ Resource: "*"
- Sid: AllowAPIGatewayReadOnly
Effect: Allow
Action:
- "apigateway:GET"
Resource:
- - "arn:*:apigateway:*::/restapis/*"
- - "arn:*:apigateway:*::/apis/*"
- - "arn:*:apigateway:*::/domainnames"
- - "arn:*:apigateway:*::/domainnames/*"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/restapis/*"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/apis/*"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames/*"
- !If
- OrganizationsEnabled
- PolicyName: ProwlerOrganizations
@@ -175,8 +263,12 @@ Resources:
Effect: Allow
Action:
- "organizations:RegisterDelegatedAdministrator"
- - "iam:CreateServiceLinkedRole"
Resource: "*"
+ - Sid: AllowCreateStackSetSLR
+ Effect: Allow
+ Action:
+ - "iam:CreateServiceLinkedRole"
+ Resource: !Sub "arn:${AWS::Partition}:iam::*:role/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com/*"
- !Ref AWS::NoValue
- !If
- S3IntegrationEnabled
@@ -219,12 +311,287 @@ Resources:
- Key: "Name"
Value: "ProwlerScan"
+ # StackSet for deploying ProwlerScan role across multiple accounts
+ ProwlerScanStackSet:
+ Type: AWS::CloudFormation::StackSet
+ Condition: DeployStackSetEnabled
+ Properties:
+ StackSetName: !Sub "${AWS::StackName}-ProwlerScan-StackSet"
+ Description: Organizational StackSet to Deploy ProwlerScan IAM Role across accounts
+ PermissionModel: SERVICE_MANAGED
+ CallAs: !If [UseDelegatedAdmin, DELEGATED_ADMIN, !Ref "AWS::NoValue"]
+ Capabilities:
+ - CAPABILITY_NAMED_IAM
+ AutoDeployment:
+ Enabled: True
+ RetainStacksOnAccountRemoval: !Ref RetainStacksOnAccountRemoval
+ OperationPreferences:
+ FailureTolerancePercentage: !Ref FailureTolerancePercentage
+ MaxConcurrentPercentage: 100
+ Parameters:
+ - ParameterKey: ExternalId
+ ParameterValue: !Ref ExternalId
+ - ParameterKey: AccountId
+ ParameterValue: !Ref AccountId
+ - ParameterKey: IAMPrincipal
+ ParameterValue: !Ref IAMPrincipal
+ - ParameterKey: EnableOrganizations
+ ParameterValue: !Ref EnableOrganizations
+ - ParameterKey: EnableS3Integration
+ ParameterValue: !Ref EnableS3Integration
+ - ParameterKey: S3IntegrationBucketName
+ ParameterValue: !Ref S3IntegrationBucketName
+ - ParameterKey: S3IntegrationBucketAccountId
+ ParameterValue: !Ref S3IntegrationBucketAccountId
+ StackInstancesGroup:
+ - DeploymentTargets:
+ OrganizationalUnitIds:
+ - !Ref AWSOrganizationalUnitId
+ Regions:
+ - us-east-1
+ TemplateBody: |
+ AWSTemplateFormatVersion: "2010-09-09"
+
+ Description: |
+ This template creates the ProwlerScan IAM Role in this account with
+ all read-only permissions to scan your account for security issues.
+ Contains two AWS managed policies (SecurityAudit and ViewOnlyAccess) and an inline policy.
+ It sets the trust policy on that IAM Role to permit Prowler to assume that role.
+ This template is designed to be used in Prowler Cloud, but can also be used in other Prowler deployments.
+
+ ** DEPLOYED VIA SERVICE-MANAGED STACKSET **
+ This stack was automatically deployed across your organization using CloudFormation StackSets
+ with SERVICE_MANAGED permissions. It will auto-deploy to new accounts and can be centrally managed.
+
+ Parameters:
+ ExternalId:
+ Description: |
+ This is the External ID that Prowler will use to assume the role ProwlerScan IAM Role.
+ Type: String
+ MinLength: 1
+ AllowedPattern: ".+"
+ ConstraintDescription: "ExternalId must not be empty."
+ AccountId:
+ Description: |
+ AWS Account ID that will assume the role created, if you are deploying this template to be used in Prowler Cloud please do not edit this.
+ Type: String
+ Default: "232136659152"
+ MinLength: 12
+ MaxLength: 12
+ AllowedPattern: "[0-9]{12}"
+ ConstraintDescription: "AccountId must be a valid AWS Account ID."
+ IAMPrincipal:
+ Description: |
+ The IAM principal type and name that will be allowed to assume the role created, leave an * for all the IAM principals in your AWS account. If you are deploying this template to be used in Prowler Cloud please do not edit this.
+ Type: String
+ Default: role/prowler*
+ EnableOrganizations:
+ Description: |
+ Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account.
+ This adds read-only Organizations permissions (e.g. ListAccounts, DescribeOrganization) and StackSet management permissions.
+ Type: String
+ Default: false
+ AllowedValues:
+ - true
+ - false
+ EnableS3Integration:
+ Description: |
+ Enable S3 integration for storing Prowler scan reports.
+ Type: String
+ Default: false
+ AllowedValues:
+ - true
+ - false
+ S3IntegrationBucketName:
+ Description: |
+ The S3 bucket name where Prowler will store scan reports for your cloud providers.
+ Type: String
+ Default: ""
+ S3IntegrationBucketAccountId:
+ Description: |
+ The AWS Account ID owner of the S3 Bucket.
+ Type: String
+ Default: ""
+
+ Conditions:
+ OrganizationsEnabled: !Equals [!Ref EnableOrganizations, true]
+ S3IntegrationEnabled: !Equals [!Ref EnableS3Integration, true]
+
+ Resources:
+ ProwlerScan:
+ Type: AWS::IAM::Role
+ Properties:
+ RoleName: ProwlerScan
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ AWS: !Sub "arn:${AWS::Partition}:iam::${AccountId}:root"
+ Action: "sts:AssumeRole"
+ Condition:
+ StringEquals:
+ "sts:ExternalId": !Sub ${ExternalId}
+ StringLike:
+ "aws:PrincipalArn": !Sub "arn:${AWS::Partition}:iam::${AccountId}:${IAMPrincipal}"
+ MaxSessionDuration: 3600
+ ManagedPolicyArns:
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/SecurityAudit"
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/job-function/ViewOnlyAccess"
+ Policies:
+ - PolicyName: ProwlerScan
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Sid: AllowMoreReadOnly
+ Effect: Allow
+ Action:
+ - "account:Get*"
+ - "appstream:Describe*"
+ - "appstream:List*"
+ - "backup:List*"
+ - "backup:Get*"
+ - "bedrock:List*"
+ - "bedrock:Get*"
+ - "cloudtrail:GetInsightSelectors"
+ - "codeartifact:List*"
+ - "codebuild:BatchGet*"
+ - "codebuild:ListReportGroups"
+ - "cognito-idp:GetUserPoolMfaConfig"
+ - "dlm:Get*"
+ - "drs:Describe*"
+ - "ds:Get*"
+ - "ds:Describe*"
+ - "ds:List*"
+ - "dynamodb:GetResourcePolicy"
+ - "ec2:GetEbsEncryptionByDefault"
+ - "ec2:GetSnapshotBlockPublicAccessState"
+ - "ec2:GetInstanceMetadataDefaults"
+ - "ecr:Describe*"
+ - "ecr:GetRegistryScanningConfiguration"
+ - "elasticfilesystem:DescribeBackupPolicy"
+ - "glue:GetConnections"
+ - "glue:GetSecurityConfiguration*"
+ - "glue:SearchTables"
+ - "glue:GetMLTransforms"
+ - "lambda:GetFunction*"
+ - "logs:FilterLogEvents"
+ - "lightsail:GetRelationalDatabases"
+ - "macie2:GetMacieSession"
+ - "macie2:GetAutomatedDiscoveryConfiguration"
+ - "s3:GetAccountPublicAccessBlock"
+ - "shield:DescribeProtection"
+ - "shield:GetSubscriptionState"
+ - "securityhub:GetFindings"
+ - "servicecatalog:Describe*"
+ - "servicecatalog:List*"
+ - "ssm:GetDocument"
+ - "ssm-incidents:List*"
+ - "states:ListTagsForResource"
+ - "support:Describe*"
+ - "tag:GetTagKeys"
+ - "wellarchitected:List*"
+ Resource: "*"
+ - Sid: AllowSecurityHubImportFindings
+ Effect: Allow
+ Action:
+ - "securityhub:BatchImportFindings"
+ Resource: "*"
+ - Sid: AllowAPIGatewayReadOnly
+ Effect: Allow
+ Action:
+ - "apigateway:GET"
+ Resource:
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/restapis/*"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/apis/*"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames"
+ - !Sub "arn:${AWS::Partition}:apigateway:*::/domainnames/*"
+ - !If
+ - OrganizationsEnabled
+ - PolicyName: ProwlerOrganizations
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Sid: AllowOrganizationsReadOnly
+ Effect: Allow
+ Action:
+ - "organizations:DescribeAccount"
+ - "organizations:DescribeOrganization"
+ - "organizations:ListAccounts"
+ - "organizations:ListAccountsForParent"
+ - "organizations:ListOrganizationalUnitsForParent"
+ - "organizations:ListRoots"
+ - "organizations:ListTagsForResource"
+ Resource: "*"
+ - Sid: AllowStackSetManagement
+ Effect: Allow
+ Action:
+ - "organizations:RegisterDelegatedAdministrator"
+ Resource: "*"
+ - Sid: AllowCreateStackSetSLR
+ Effect: Allow
+ Action:
+ - "iam:CreateServiceLinkedRole"
+ Resource: !Sub "arn:${AWS::Partition}:iam::*:role/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com/*"
+ - !Ref AWS::NoValue
+ - !If
+ - S3IntegrationEnabled
+ - PolicyName: S3Integration
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action:
+ - "s3:PutObject"
+ Resource:
+ - !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}/*"
+ Condition:
+ StringEquals:
+ "s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
+ - Effect: Allow
+ Action:
+ - "s3:ListBucket"
+ Resource:
+ - !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}"
+ Condition:
+ StringEquals:
+ "s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
+ - Effect: Allow
+ Action:
+ - "s3:DeleteObject"
+ Resource:
+ - !Sub "arn:${AWS::Partition}:s3:::${S3IntegrationBucketName}/*test-prowler-connection.txt"
+ Condition:
+ StringEquals:
+ "s3:ResourceAccount": !Sub ${S3IntegrationBucketAccountId}
+ - !Ref AWS::NoValue
+ Tags:
+ - Key: "Service"
+ Value: "https://prowler.com"
+ - Key: "Support"
+ Value: "support@prowler.com"
+ - Key: "CloudFormation"
+ Value: "true"
+ - Key: "Name"
+ Value: "ProwlerScan"
+
+ Outputs:
+ ProwlerScanRoleArn:
+ Description: "ARN of the ProwlerScan IAM Role"
+ Value: !GetAtt ProwlerScan.Arn
+ Export:
+ Name: !Sub "${AWS::StackName}-ProwlerScanRoleArn"
+
Metadata:
- AWS::CloudFormation::StackName: "Prowler"
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
- default: Required
+ default: Deployment Options
+ Parameters:
+ - DeployLocalRole
+ - DeployStackSet
+ - Label:
+ default: Required Prowler Configuration
Parameters:
- ExternalId
- AccountId
@@ -232,14 +599,27 @@ Metadata:
- EnableOrganizations
- EnableS3Integration
- Label:
- default: Optional
+ default: Optional S3 Integration
Parameters:
- S3IntegrationBucketName
- S3IntegrationBucketAccountId
+ - Label:
+ default: StackSet Configuration (Required if DeployStackSet is true)
+ Parameters:
+ - AWSOrganizationalUnitId
+ - DeployFromDelegatedAdmin
+ - RetainStacksOnAccountRemoval
+ - FailureTolerancePercentage
Outputs:
- ProwlerScanRoleArn:
- Description: "ARN of the ProwlerScan IAM Role"
+ LocalProwlerScanRoleArn:
+ Condition: DeployLocalRoleEnabled
+ Description: "ARN of the ProwlerScan IAM Role deployed locally in this account"
Value: !GetAtt ProwlerScan.Arn
Export:
Name: !Sub "${AWS::StackName}-ProwlerScanRoleArn"
+
+ StackSetId:
+ Condition: DeployStackSetEnabled
+ Description: "StackSet ID for the ProwlerScan role deployment across accounts"
+ Value: !Ref ProwlerScanStackSet
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 7a98933c5f..c019e8d087 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -4,6 +4,169 @@ All notable changes to the **Prowler SDK** are documented in this file.
+## [5.38.0] (Prowler v5.38.0)
+
+### 🚀 Added
+
+- `admincenter_shared_bookings_disabled` check for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 control 1.3.9 [(#12147)](https://github.com/prowler-cloud/prowler/pull/12147)
+- `defender_priority_account_protection_enabled` and `defender_strict_preset_security_policy_enabled` checks for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 controls 2.4.1 and 2.4.2 [(#12148)](https://github.com/prowler-cloud/prowler/pull/12148)
+- `exchange_owa_mailbox_policy_personal_accounts_disabled` and `exchange_organization_reject_direct_send_enabled` checks for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 controls 6.3.2 and 6.5.5 [(#12149)](https://github.com/prowler-cloud/prowler/pull/12149)
+- `teams_external_access_trial_tenants_blocked` check for M365 provider, verifying that Teams external access with trial-only tenants is blocked, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 control 8.2.4 [(#12151)](https://github.com/prowler-cloud/prowler/pull/12151)
+- `entra_device_registration_join_restricted`, `entra_device_registration_max_devices_per_user_limited`, `entra_device_registration_global_admins_not_local_admins`, `entra_device_registration_registering_user_not_local_admin`, `entra_device_registration_laps_enabled` and `entra_policy_default_user_cannot_read_bitlocker_keys` checks for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 device registration controls (5.1.4.x) [(#12152)](https://github.com/prowler-cloud/prowler/pull/12152)
+- The IAM privilege-escalation check now detects 22 additional pathfinding.cloud escalation paths across AWS Batch, Braket, CodeDeploy, Cognito Identity, ECS, EMR, EMR Serverless, GameLift, Glue, EC2 Image Builder, Kinesis Analytics, HealthOmics, EventBridge Scheduler, SSM Automation, Step Functions, IAM permissions boundaries, and IAM Identity Center (SSO) [(#12237)](https://github.com/prowler-cloud/prowler/pull/12237)
+- Container images now ship an SBOM and build provenance as OCI attestations [(#12352)](https://github.com/prowler-cloud/prowler/pull/12352)
+
+### 🔄 Changed
+
+- Highlighted key security terms in the Risk description of 8 existing M365 checks [(#12156)](https://github.com/prowler-cloud/prowler/pull/12156)
+- Moved the Trivy suppressions from the classic `.trivyignore` to `.trivyignore.yaml`, so each entry is scoped to the package it names instead of suppressing its CVE across the whole image [(#12314)](https://github.com/prowler-cloud/prowler/pull/12314)
+- The `securityhub_delegated_admin_enabled_all_regions`, `guardduty_delegated_admin_enabled_all_regions` and `config_delegated_admin_and_org_aggregator_all_regions` checks now report MANUAL instead of FAIL when the delegated administrator status cannot be read and no independent misconfiguration is detected, which happens on member accounts that are not registered as delegated administrators because the API is restricted to the organization management account and to delegated administrator accounts [(#12319)](https://github.com/prowler-cloud/prowler/pull/12319)
+- Pin the container vulnerability scanner to Trivy v0.72.0, matching prowler-registry and partner-portal [(#12346)](https://github.com/prowler-cloud/prowler/pull/12346)
+- Quote the unquoted shell expansions in the release and build workflows [(#12365)](https://github.com/prowler-cloud/prowler/pull/12365)
+- Fix the remaining shellcheck findings in workflows and enable the check [(#12367)](https://github.com/prowler-cloud/prowler/pull/12367)
+
+### 🐞 Fixed
+
+- Spurious error log output from `Get-ApplicationAccessPolicy` on M365 tenants without application access policies [(#12149)](https://github.com/prowler-cloud/prowler/pull/12149)
+- Secret checks no longer report credential-free JDBC connection strings as embedded credentials [(#12288)](https://github.com/prowler-cloud/prowler/pull/12288)
+- A failed `ListOrganizationAdminAccounts` lookup in one region no longer marks the Security Hub delegated administrator status as undetermined in every other region [(#12319)](https://github.com/prowler-cloud/prowler/pull/12319)
+- `securityhub_delegated_admin_enabled_all_regions` no longer reports FAIL with `delegated administrator status could not be determined` on accounts that do have a Security Hub delegated administrator; `ListOrganizationAdminAccounts` responses are now parsed with the `AccountId` and `Status` fields the API actually returns [(#12319)](https://github.com/prowler-cloud/prowler/pull/12319)
+- `guardduty_delegated_admin_enabled_all_regions` no longer reports `no delegated administrator configured` when the lookup was denied or failed, which asserted absence where there was only lack of visibility [(#12319)](https://github.com/prowler-cloud/prowler/pull/12319)
+- OCI Identity service no longer drops the whole dynamic groups, groups, policies or users listing when the OCI API returns null optional fields such as `matching_rule` [(#12327)](https://github.com/prowler-cloud/prowler/pull/12327)
+- Alibaba Cloud STS credential validation retries transient connection failures and reports exhausted attempts as connection errors instead of invalid credentials [(#12353)](https://github.com/prowler-cloud/prowler/pull/12353)
+
+### 🔐 Security
+
+- Bumped the Compose DozerDB image from 5.26.3.0 to 5.26.27.0, which moves it off Debian 11 and onto Debian 13 [(#12320)](https://github.com/prowler-cloud/prowler/pull/12320)
+- The SDK container image now verifies the checksum of every third-party binary it downloads (PowerShell, Trivy, zizmor) before installing it [(#12334)](https://github.com/prowler-cloud/prowler/pull/12334)
+- Upgrade aiohttp to 3.14.3 to pick up the fix for CVE-2026-69244 [(#12340)](https://github.com/prowler-cloud/prowler/pull/12340)
+- Upgrade cryptography to 50.0.0, closing CVE-2026-69247 and CVE-2026-69249 [(#12356)](https://github.com/prowler-cloud/prowler/pull/12356)
+
+---
+
+## [5.37.1] (Prowler v5.37.1)
+
+### 🔄 Changed
+
+- Huawei Cloud exception codes moved from `19000`-`19007` to `20000`-`20007`, resolving a collision with E2E Networks which reserves `19000`-`19999` [(#12306)](https://github.com/prowler-cloud/prowler/pull/12306)
+
+### 🐞 Fixed
+
+- Checks registered through the `prowler.checks.` entry-point group can now run against built-in providers. The built-in probe in `_resolve_check_module` used a bare `find_spec`, which imports the parent package to search it and so raised `ModuleNotFoundError` for a plug-in check instead of returning `None`, aborting the lookup before the entry points were consulted. Such a check was discovered, listed and selected for execution, then silently produced no findings. [(#12312)](https://github.com/prowler-cloud/prowler/pull/12312)
+- Entra Conditional Access guest-user checks no longer report false FAILs: microsoft-kiota packages bumped to 1.9.10 so `guestOrExternalUserTypes` (a flags enum Graph serializes as a comma-separated string) deserializes correctly instead of returning an empty list [(#12315)](https://github.com/prowler-cloud/prowler/pull/12315)
+
+### 🔐 Security
+
+- Bumped the Compose `postgres` and `valkey` images, clearing 10 critical CVEs [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- Bumped PowerShell, Trivy, uv and `joserfc` in the container images, clearing 14 high-severity CVEs from the SDK and API images [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- Bumped `httplib2` to 0.32.0 and `pyasn1` to 0.6.4 to resolve known CVEs [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- The SDK container image now builds on Debian 13 (trixie), clearing the unfixable `libsqlite3-0` and `zlib1g` criticals [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- Bumped `cryptography` to 48.0.1 to resolve GHSA-537c-gmf6-5ccf, along with the `oci`, `alibabacloud-tea-openapi`, `darabonba-core` and `py-ocsf-models` bumps it requires [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- Removed `pip` from the SDK container image, clearing two high-severity CVEs in the vendored copies of `setuptools` and `msgpack` [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+- Removed `wget`, `gnupg` and `apt-transport-https` from the SDK runtime image [(#12307)](https://github.com/prowler-cloud/prowler/pull/12307)
+
+---
+
+## [5.37.0] (Prowler v5.37.0)
+
+### 🚀 Added
+
+- OCSF detection finding output now populates `finding_info.analytic` as the Prowler check rule and `finding_info.attacks` as MITRE ATT&CK technique and tactic objects for findings with MITRE-ATTACK compliance metadata [(#11492)](https://github.com/prowler-cloud/prowler/pull/11492)
+- `codecommit` service and `codecommit_repository_no_secrets` check for AWS provider, scanning files tracked at the tip of each repository's default branch for hardcoded secrets [(#11846)](https://github.com/prowler-cloud/prowler/pull/11846)
+- Huawei Cloud provider, with CTS, ECS, ELB, EVS, IAM, KMS, OBS, RDS, VPC and WAF services and a CIS 1.0 compliance benchmark [(#11950)](https://github.com/prowler-cloud/prowler/pull/11950)
+- `glue_catalog_connection_no_secrets` check to detect secrets in Glue Data Catalog connection properties [(#11963)](https://github.com/prowler-cloud/prowler/pull/11963)
+- `ec2_instance_stopped_older_than_specific_days` check for AWS provider, detecting EC2 instances stopped longer than a configurable number of days (default 30) [(#12076)](https://github.com/prowler-cloud/prowler/pull/12076)
+- `sagemaker_endpoint_config_kms_encryption_enabled` check verifying SageMaker endpoint configurations use a KMS key for storage volume encryption [(#12118)](https://github.com/prowler-cloud/prowler/pull/12118)
+- 11 AWS Nitro Enclaves security checks providing the first CSPM coverage for confidential computing workloads, covering both host environment (`ec2_confidential_workload_host_*`) and KMS attestation policy (`kms_key_enclave_*`), fully passive via boto3 and CloudTrail LookupEvents [(#12283)](https://github.com/prowler-cloud/prowler/pull/12283)
+
+### 🐞 Fixed
+
+- Scan configuration schema no longer exposes SDK/CLI-only providers such as `e2enetworks`; the aggregated schema served by `/scan-configurations/schema` now includes only app providers (`sdk_only = False`) [(#12094)](https://github.com/prowler-cloud/prowler/pull/12094)
+- GCP Cloud Functions gen2 IAM policy retrieval now uses a per-request HTTP client, preventing a process crash from concurrent thread-unsafe `httplib2` access when a project has several gen2 functions [(#12107)](https://github.com/prowler-cloud/prowler/pull/12107)
+- GCP firewall SSH and RDP checks now detect exposed target ports in any position within multi-port rules [(#12115)](https://github.com/prowler-cloud/prowler/pull/12115)
+- Secret ignore patterns now use Kingfisher-compatible LF line indexing for scanned content containing ASCII control characters [(#12141)](https://github.com/prowler-cloud/prowler/pull/12141)
+- Jira descriptions with inline code nested in bold or italic Markdown now render as valid ADF [(#12158)](https://github.com/prowler-cloud/prowler/pull/12158)
+
+### 🔐 Security
+
+- HTML reports escape provider-originated finding fields to prevent stored cross-site scripting through malicious cloud resource tags [(#12221)](https://github.com/prowler-cloud/prowler/pull/12221)
+
+---
+
+## [5.36.0] (Prowler v5.36.0)
+
+### 🚀 Added
+
+- `sagemaker_notebook_instance_no_secrets` check for AWS provider, scanning SageMaker notebook instance lifecycle configuration scripts (`OnCreate` and `OnStart`) for hardcoded secrets such as API keys, passwords, tokens, and connection strings [(#11843)](https://github.com/prowler-cloud/prowler/pull/11843)
+
+### 🔄 Changed
+
+- Jira output rendering supports grouped Finding Group issues with caller-provided links and capped or uncapped finding copy [(#12035)](https://github.com/prowler-cloud/prowler/pull/12035)
+
+### 🐞 Fixed
+
+- Fix invalid escape sequence `SyntaxWarning` raised on startup by the S3 bucket name validation regex [(#12041)](https://github.com/prowler-cloud/prowler/pull/12041)
+- Alibaba Cloud SSH and RDP security group checks no longer produce false negatives when allowed rules use capitalized `Policy="Accept"` values [(#12049)](https://github.com/prowler-cloud/prowler/pull/12049)
+
+---
+
+## [5.35.0] (Prowler v5.35.0)
+
+### 🚀 Added
+
+- `excluded_checks` and `excluded_services` in scan configurations to narrow the execution scope [(#12028)](https://github.com/prowler-cloud/prowler/pull/12028)
+
+### 🔐 Security
+
+- Jira tenant information requests validate site names and do not follow redirects [(#12012)](https://github.com/prowler-cloud/prowler/pull/12012)
+
+---
+
+## [5.34.0] (Prowler v5.34.0)
+
+### 🚀 Added
+
+- `elbv2_listener_pqc_tls_enabled` check for AWS provider, verifying that ELBv2 listeners use post-quantum TLS policies [(#11254)](https://github.com/prowler-cloud/prowler/pull/11254)
+- `iaas_server_public_ip_attached` check for STACKIT provider, flagging IaaS servers that have a public IP address directly attached to a network interface [(#11549)](https://github.com/prowler-cloud/prowler/pull/11549)
+- Changelog fragment workflow for SDK, API, UI, and MCP Server releases, including PR attribution, fragment validation, release compilation, and preserved section ordering [(#11572)](https://github.com/prowler-cloud/prowler/pull/11572)
+- E2E Networks provider with 27 checks across compute nodes, networking, security groups, load balancers, block/file storage, and managed databases [(#11654)](https://github.com/prowler-cloud/prowler/pull/11654)
+- `datapipeline_pipeline_no_secrets_in_definition` check for AWS provider, scanning Data Pipeline object fields, parameter objects, and parameter values for hardcoded secrets with Kingfisher [(#11821)](https://github.com/prowler-cloud/prowler/pull/11821)
+- `amplify_app_no_secrets_in_environment` check for AWS provider, scanning Amplify app and branch environment variables and build settings for hardcoded secrets [(#11825)](https://github.com/prowler-cloud/prowler/pull/11825)
+- `ec2_ami_account_block_public_access` check for AWS provider, verifying AMI block public access is enabled at the account level in each Region so AMIs cannot be shared publicly [(#11828)](https://github.com/prowler-cloud/prowler/pull/11828)
+- `core_readonly_root_filesystem_enabled` check for Kubernetes provider, verifying that every container in each Pod explicitly sets `readOnlyRootFilesystem: true` in its security context [(#11835)](https://github.com/prowler-cloud/prowler/pull/11835)
+- `core_minimize_hostpath_volume_mounts` check for Kubernetes provider, detecting Pods that use `hostPath` volumes [(#11837)](https://github.com/prowler-cloud/prowler/pull/11837)
+- `app_function_ensure_http_is_redirected_to_https` check for Azure provider, verifying that Function Apps enforce HTTPS-only traffic [(#11929)](https://github.com/prowler-cloud/prowler/pull/11929)
+
+### 🔄 Changed
+
+- Add missing trailing newlines to compliance, region, and fixture data files for POSIX compliance [(#11765)](https://github.com/prowler-cloud/prowler/pull/11765)
+- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11853)](https://github.com/prowler-cloud/prowler/pull/11853)
+- Redesign the local dashboard sidebar and informational pages [(#11972)](https://github.com/prowler-cloud/prowler/pull/11972)
+
+---
+
+## [5.33.2] (Prowler v5.33.2)
+
+### 🐞 Fixed
+
+- EC2 AMI loading now targets Amazon-owned AMIs used by audited instances, reducing AWS API calls during EC2 scans [(#11958)](https://github.com/prowler-cloud/prowler/pull/11958)
+- `ec2_instance_account_imdsv2_enabled` findings now use regional resource ARNs, preventing findings from different AWS Regions from collapsing into one resource [(#11966)](https://github.com/prowler-cloud/prowler/pull/11966)
+
+---
+
+## [5.33.1] (Prowler v5.33.1)
+
+### 🐞 Fixed
+
+- ECS task definition resource limits now select the latest task definitions by registration date instead of relying on ARN ordering [(#11891)](https://github.com/prowler-cloud/prowler/pull/11891)
+- `dlm_ebs_snapshot_lifecycle_policy_exists` no longer initializes the full EC2 inventory just to detect EBS snapshots, avoiding slow scans when checking DLM lifecycle policies [(#11900)](https://github.com/prowler-cloud/prowler/pull/11900)
+- `dms_instance_no_public_access` no longer initializes the full EC2 service when there are no DMS replication instances [(#11902)](https://github.com/prowler-cloud/prowler/pull/11902)
+- `organizations_scp_check_deny_regions` no longer reports false `FAIL` for AWS Organizations that restrict regions with Allow-based SCPs; the Allow path now checks the statement `Effect` instead of an always-false comparison that made it unreachable [(#11915)](https://github.com/prowler-cloud/prowler/pull/11915)
+- Jira issue creation failures now preserve safe structured response details from Jira [(#11925)](https://github.com/prowler-cloud/prowler/pull/11925)
+- Azure Function App optional permission failures now log as warnings, and Function App environment variable fields use the correct spelling internally [(#11926)](https://github.com/prowler-cloud/prowler/pull/11926)
+
+---
+
## [5.33.0] (Prowler v5.33.0)
### 🐞 Fixed
diff --git a/prowler/__main__.py b/prowler/__main__.py
index 12f4b24cac..2d77068704 100644
--- a/prowler/__main__.py
+++ b/prowler/__main__.py
@@ -144,6 +144,7 @@ from prowler.providers.e2enetworks.models import E2eNetworksOutputOptions
from prowler.providers.gcp.models import GCPOutputOptions
from prowler.providers.github.models import GithubOutputOptions
from prowler.providers.googleworkspace.models import GoogleWorkspaceOutputOptions
+from prowler.providers.huaweicloud.models import HuaweiCloudOutputOptions
from prowler.providers.iac.models import IACOutputOptions
from prowler.providers.image.exceptions.exceptions import ImageBaseException
from prowler.providers.image.models import ImageOutputOptions
@@ -449,6 +450,10 @@ def prowler():
output_options = LinodeOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
+ elif provider == "huaweicloud":
+ output_options = HuaweiCloudOutputOptions(
+ args, bulk_checks_metadata, global_provider.identity
+ )
else:
# Dynamic fallback: any external/custom provider
try:
diff --git a/prowler/changelog.d/11572.added.md b/prowler/changelog.d/11572.added.md
deleted file mode 100644
index 4838752200..0000000000
--- a/prowler/changelog.d/11572.added.md
+++ /dev/null
@@ -1 +0,0 @@
-Changelog fragment workflow for SDK, API, UI, and MCP Server releases, including PR attribution, fragment validation, release compilation, and preserved section ordering
diff --git a/prowler/changelog.d/awslambda-layer-no-secrets-in-content.added.md b/prowler/changelog.d/awslambda-layer-no-secrets-in-content.added.md
new file mode 100644
index 0000000000..56a192b088
--- /dev/null
+++ b/prowler/changelog.d/awslambda-layer-no-secrets-in-content.added.md
@@ -0,0 +1 @@
+`awslambda_layer_no_secrets_in_content` check for AWS provider, scanning Lambda layer package content for hardcoded secrets
diff --git a/prowler/changelog.d/batch-job-definition-no-secrets.added.md b/prowler/changelog.d/batch-job-definition-no-secrets.added.md
new file mode 100644
index 0000000000..797dc06a12
--- /dev/null
+++ b/prowler/changelog.d/batch-job-definition-no-secrets.added.md
@@ -0,0 +1 @@
+`batch_job_definition_no_secrets` check for AWS provider, scanning Batch job definition environment variables and command parameters for hardcoded secrets
diff --git a/prowler/changelog.d/e2enetworks-provider.added.md b/prowler/changelog.d/e2enetworks-provider.added.md
deleted file mode 100644
index 9185c9d51c..0000000000
--- a/prowler/changelog.d/e2enetworks-provider.added.md
+++ /dev/null
@@ -1 +0,0 @@
-E2E Networks provider with 27 checks across compute nodes, networking, security groups, load balancers, block/file storage, and managed databases
diff --git a/prowler/changelog.d/ecs-task-definitions-registration-date.fixed.md b/prowler/changelog.d/ecs-task-definitions-registration-date.fixed.md
deleted file mode 100644
index 6f470e4873..0000000000
--- a/prowler/changelog.d/ecs-task-definitions-registration-date.fixed.md
+++ /dev/null
@@ -1 +0,0 @@
-ECS task definition resource limits now select the latest task definitions by registration date instead of relying on ARN ordering
diff --git a/prowler/changelog.d/m365-cis7-entra-directory-settings.added.md b/prowler/changelog.d/m365-cis7-entra-directory-settings.added.md
new file mode 100644
index 0000000000..4fc76e535f
--- /dev/null
+++ b/prowler/changelog.d/m365-cis7-entra-directory-settings.added.md
@@ -0,0 +1 @@
+7 M365 Entra checks covering CIS Microsoft 365 Foundations Benchmark v7.0.0 password protection, default user permissions, and guest invitation domain restrictions
diff --git a/prowler/changelog.d/oci-regionless-platform-11565.changed.md b/prowler/changelog.d/oci-regionless-platform-11565.changed.md
deleted file mode 100644
index 1f61fc5f55..0000000000
--- a/prowler/changelog.d/oci-regionless-platform-11565.changed.md
+++ /dev/null
@@ -1 +0,0 @@
-Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided
diff --git a/prowler/compliance/aws/aws_ai_security_framework_aws.json b/prowler/compliance/aws/aws_ai_security_framework_aws.json
index 9b87f7464d..f20baf4735 100644
--- a/prowler/compliance/aws/aws_ai_security_framework_aws.json
+++ b/prowler/compliance/aws/aws_ai_security_framework_aws.json
@@ -187,10 +187,15 @@
"Section": "Infrastructure Security",
"SubSection": "Compute Isolation",
"Service": "ec2",
- "Type": "Manual"
+ "Type": "Automated"
}
],
- "Checks": []
+ "Checks": [
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_attestation_no_deployment_binding",
+ "kms_key_enclave_attestation_pcr_mismatch"
+ ]
},
{
"Id": "AISF-IAM-01",
@@ -899,7 +904,9 @@
"Checks": [
"cloudtrail_threat_detection_llm_jacking",
"cloudtrail_threat_detection_privilege_escalation",
- "cloudtrail_threat_detection_enumeration"
+ "cloudtrail_threat_detection_enumeration",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
]
},
{
diff --git a/prowler/compliance/aws/aws_well_architected_framework_reliability_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_reliability_pillar_aws.json
index db334a690a..607fdc7192 100644
--- a/prowler/compliance/aws/aws_well_architected_framework_reliability_pillar_aws.json
+++ b/prowler/compliance/aws/aws_well_architected_framework_reliability_pillar_aws.json
@@ -53,7 +53,8 @@
"opensearch_service_domains_audit_logging_enabled",
"opensearch_service_domains_cloudwatch_logging_enabled",
"rds_instance_enhanced_monitoring_enabled",
- "rds_instance_integration_cloudwatch_logs"
+ "rds_instance_integration_cloudwatch_logs",
+ "ec2_confidential_workload_host_not_running"
]
},
{
diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json
index a025bb3a3c..41458eea5b 100644
--- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json
+++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json
@@ -324,7 +324,8 @@
"Checks": [
"ec2_instance_imdsv2_enabled",
"ec2_instance_profile_attached",
- "cloudwatch_cross_account_sharing_disabled"
+ "cloudwatch_cross_account_sharing_disabled",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
]
},
{
@@ -497,7 +498,8 @@
"sqs_queues_not_publicly_accessible",
"ssm_documents_set_as_public",
"ec2_securitygroup_allow_wide_open_public_ipv4",
- "ec2_ami_public"
+ "ec2_ami_public",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -833,7 +835,8 @@
"ec2_instance_internet_facing_with_instance_profile",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"redshift_cluster_automatic_upgrades",
- "ssm_managed_compliant_patching"
+ "ssm_managed_compliant_patching",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
]
},
{
@@ -1181,6 +1184,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
diff --git a/prowler/compliance/aws/c5_aws.json b/prowler/compliance/aws/c5_aws.json
index 269a5ce308..ce26e241e3 100644
--- a/prowler/compliance/aws/c5_aws.json
+++ b/prowler/compliance/aws/c5_aws.json
@@ -2439,7 +2439,8 @@
"ssm_documents_set_as_public",
"vpc_subnet_no_public_ip_by_default",
"vpc_subnet_separate_private_public",
- "workspaces_vpc_2private_1public_subnets_nat"
+ "workspaces_vpc_2private_1public_subnets_nat",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -6729,7 +6730,8 @@
"kms_cmk_not_deleted_unintentionally",
"kms_cmk_not_multi_region",
"kms_key_not_publicly_accessible",
- "ec2_ebs_volume_encryption"
+ "ec2_ebs_volume_encryption",
+ "kms_key_enclave_attestation_not_enforced"
],
"ConfigRequirements": [
{
@@ -6840,7 +6842,8 @@
"kms_cmk_rotation_enabled",
"kms_key_not_publicly_accessible",
"s3_bucket_kms_encryption",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -7785,7 +7788,8 @@
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json
index 7935424193..ef31bfe1ea 100644
--- a/prowler/compliance/aws/ccc_aws.json
+++ b/prowler/compliance/aws/ccc_aws.json
@@ -49,6 +49,7 @@
"elb_insecure_ssl_ciphers",
"elb_ssl_listeners",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
diff --git a/prowler/compliance/aws/cis_3.0_aws.json b/prowler/compliance/aws/cis_3.0_aws.json
index 5540bc40cf..cc8c292e93 100644
--- a/prowler/compliance/aws/cis_3.0_aws.json
+++ b/prowler/compliance/aws/cis_3.0_aws.json
@@ -1258,7 +1258,8 @@
"Checks": [
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -1281,7 +1282,8 @@
"Checks": [
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -1344,7 +1346,8 @@
"Id": "5.6",
"Description": "Ensure that EC2 Metadata Service only allows IMDSv2",
"Checks": [
- "ec2_instance_imdsv2_enabled"
+ "ec2_instance_imdsv2_enabled",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
],
"Attributes": [
{
diff --git a/prowler/compliance/aws/cis_7.0_aws.json b/prowler/compliance/aws/cis_7.0_aws.json
index f4f7fedff8..a927c375bb 100644
--- a/prowler/compliance/aws/cis_7.0_aws.json
+++ b/prowler/compliance/aws/cis_7.0_aws.json
@@ -1607,4 +1607,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json
index 144437ce52..6edfc39d97 100644
--- a/prowler/compliance/aws/ens_rd2022_aws.json
+++ b/prowler/compliance/aws/ens_rd2022_aws.json
@@ -2289,7 +2289,8 @@
}
],
"Checks": [
- "ec2_securitygroup_allow_ingress_from_internet_to_all_ports"
+ "ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -2495,6 +2496,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
@@ -2521,6 +2523,7 @@
],
"Checks": [
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
@@ -4325,7 +4328,9 @@
"Dependencias": []
}
],
- "Checks": []
+ "Checks": [
+ "kms_key_enclave_attestation_not_enforced"
+ ]
},
{
"Id": "op.exp.10.aws.cmk.7",
@@ -4349,7 +4354,9 @@
"Dependencias": []
}
],
- "Checks": []
+ "Checks": [
+ "kms_key_enclave_attestation_bypassable_path"
+ ]
},
{
"Id": "op.exp.10.aws.cmk.8",
@@ -4373,7 +4380,9 @@
"Dependencias": []
}
],
- "Checks": []
+ "Checks": [
+ "kms_key_enclave_attestation_pcr_mismatch"
+ ]
},
{
"Id": "op.cont.2.aws.az.1",
diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
index 15763ef48e..ebc7d696c9 100644
--- a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
+++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
@@ -79,7 +79,8 @@
"rds_cluster_multi_az",
"vpc_subnet_auto_assign_public_ip_disabled",
"vpc_default_security_group_restricts_traffic",
- "vpc_peering_connection_routing_tables_with_least_privilege"
+ "vpc_peering_connection_routing_tables_with_least_privilege",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
]
},
{
@@ -202,7 +203,9 @@
"redshift_cluster_audit_logging",
"s3_bucket_server_access_logging_enabled",
"vpc_flow_logs_enabled",
- "wafv2_webacl_logging_enabled"
+ "wafv2_webacl_logging_enabled",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
]
},
{
@@ -310,7 +313,8 @@
"s3_bucket_secure_transport_policy",
"sagemaker_notebook_instance_encryption_enabled",
"sns_topics_kms_encryption_at_rest_enabled",
- "sqs_queue_server_side_encryption_enabled"
+ "sqs_queue_server_side_encryption_enabled",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json
index eaa3ea25dc..06f81ea08d 100644
--- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json
+++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json
@@ -313,7 +313,9 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -343,7 +345,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -424,7 +427,9 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path"
]
},
{
@@ -754,7 +759,8 @@
"guardduty_is_enabled",
"rds_instance_enhanced_monitoring_enabled",
"redshift_cluster_audit_logging",
- "securityhub_enabled"
+ "securityhub_enabled",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
],
"ConfigRequirements": [
{
@@ -821,7 +827,8 @@
],
"Checks": [
"ec2_instance_managed_by_ssm",
- "ssm_managed_compliant_patching"
+ "ssm_managed_compliant_patching",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -1324,7 +1331,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -1359,7 +1367,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -1377,6 +1386,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -1399,6 +1409,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -1420,7 +1431,9 @@
],
"Checks": [
"acm_certificates_expiration_check",
- "kms_cmk_rotation_enabled"
+ "kms_cmk_rotation_enabled",
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path"
]
},
{
@@ -1482,7 +1495,8 @@
"s3_bucket_default_encryption",
"s3_bucket_default_encryption",
"sagemaker_notebook_instance_encryption_enabled",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_debug_attestation_detected"
]
},
{
@@ -1587,7 +1601,9 @@
"ec2_instance_imdsv2_enabled",
"guardduty_is_enabled",
"redshift_cluster_audit_logging",
- "securityhub_enabled"
+ "securityhub_enabled",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_debug_attestation_detected"
],
"ConfigRequirements": [
{
@@ -1756,7 +1772,8 @@
}
],
"Checks": [
- "cloudtrail_log_file_validation_enabled"
+ "cloudtrail_log_file_validation_enabled",
+ "kms_key_enclave_attestation_pcr_mismatch"
]
},
{
diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json
index 8a50b79925..f9e72adf0e 100644
--- a/prowler/compliance/aws/ffiec_aws.json
+++ b/prowler/compliance/aws/ffiec_aws.json
@@ -615,6 +615,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -878,7 +879,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
diff --git a/prowler/compliance/aws/gdpr_aws.json b/prowler/compliance/aws/gdpr_aws.json
index a97a11e3dc..1eae6ec040 100644
--- a/prowler/compliance/aws/gdpr_aws.json
+++ b/prowler/compliance/aws/gdpr_aws.json
@@ -58,7 +58,8 @@
"cloudwatch_log_metric_filter_root_usage",
"cloudwatch_log_metric_filter_security_group_changes",
"cloudwatch_log_metric_filter_unauthorized_api_calls",
- "vpc_flow_logs_enabled"
+ "vpc_flow_logs_enabled",
+ "kms_key_enclave_attestation_not_enforced"
],
"ConfigRequirements": [
{
@@ -139,7 +140,8 @@
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
"sagemaker_notebook_instance_encryption_enabled",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_attestation_not_enforced"
]
}
]
diff --git a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json
index 871af9e726..f4bae0b9d6 100644
--- a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json
+++ b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json
@@ -109,7 +109,9 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -195,7 +197,9 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -266,6 +270,7 @@
"ec2_ebs_default_encryption",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
diff --git a/prowler/compliance/aws/hipaa_aws.json b/prowler/compliance/aws/hipaa_aws.json
index 9eb243e6cc..f2cf10c666 100644
--- a/prowler/compliance/aws/hipaa_aws.json
+++ b/prowler/compliance/aws/hipaa_aws.json
@@ -85,7 +85,8 @@
"sns_topics_kms_encryption_at_rest_enabled",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -115,7 +116,8 @@
"redshift_cluster_audit_logging",
"s3_bucket_server_access_logging_enabled",
"securityhub_enabled",
- "vpc_flow_logs_enabled"
+ "vpc_flow_logs_enabled",
+ "kms_key_enclave_debug_attestation_detected"
],
"ConfigRequirements": [
{
@@ -641,7 +643,10 @@
"s3_bucket_public_access",
"s3_bucket_policy_public_write_access",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path"
]
},
{
@@ -715,7 +720,8 @@
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
"sagemaker_notebook_instance_encryption_enabled",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -746,7 +752,8 @@
"redshift_cluster_audit_logging",
"s3_bucket_server_access_logging_enabled",
"securityhub_enabled",
- "vpc_flow_logs_enabled"
+ "vpc_flow_logs_enabled",
+ "kms_key_enclave_debug_attestation_detected"
],
"ConfigRequirements": [
{
@@ -780,7 +787,8 @@
"ec2_ebs_volume_encryption",
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
- "s3_bucket_object_versioning"
+ "s3_bucket_object_versioning",
+ "kms_key_enclave_attestation_pcr_mismatch"
]
},
{
@@ -801,7 +809,8 @@
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
"s3_bucket_object_versioning",
- "vpc_flow_logs_enabled"
+ "vpc_flow_logs_enabled",
+ "kms_key_enclave_attestation_unknown_image"
]
},
{
@@ -843,7 +852,9 @@
"s3_bucket_secure_transport_policy",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_unrestricted_ingress",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json
index 1de8c23db8..c7ce030024 100644
--- a/prowler/compliance/aws/iso27001_2013_aws.json
+++ b/prowler/compliance/aws/iso27001_2013_aws.json
@@ -36,6 +36,7 @@
"Checks": [
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled"
diff --git a/prowler/compliance/aws/iso27001_2022_aws.json b/prowler/compliance/aws/iso27001_2022_aws.json
index 563b856317..d245053375 100644
--- a/prowler/compliance/aws/iso27001_2022_aws.json
+++ b/prowler/compliance/aws/iso27001_2022_aws.json
@@ -1190,7 +1190,8 @@
],
"Checks": [
"guardduty_is_enabled",
- "guardduty_no_high_severity_findings"
+ "guardduty_no_high_severity_findings",
+ "kms_key_enclave_attestation_pcr_mismatch"
],
"ConfigRequirements": [
{
@@ -1491,7 +1492,9 @@
"cloudwatch_log_metric_filter_root_usage",
"cloudwatch_log_metric_filter_security_group_changes",
"cloudwatch_log_metric_filter_sign_in_without_mfa",
- "cloudwatch_log_metric_filter_unauthorized_api_calls"
+ "cloudwatch_log_metric_filter_unauthorized_api_calls",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
]
},
{
@@ -1628,7 +1631,11 @@
"ec2_securitygroup_from_launch_wizard",
"ec2_securitygroup_not_used",
"ec2_securitygroup_with_many_ingress_egress_rules",
- "ec2_transitgateway_auto_accept_vpc_attachments"
+ "ec2_transitgateway_auto_accept_vpc_attachments",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -1723,7 +1730,10 @@
"ec2_securitygroup_from_launch_wizard",
"ec2_securitygroup_not_used",
"ec2_securitygroup_with_many_ingress_egress_rules",
- "ec2_transitgateway_auto_accept_vpc_attachments"
+ "ec2_transitgateway_auto_accept_vpc_attachments",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -1818,7 +1828,10 @@
"ec2_securitygroup_from_launch_wizard",
"ec2_securitygroup_not_used",
"ec2_securitygroup_with_many_ingress_egress_rules",
- "ec2_transitgateway_auto_accept_vpc_attachments"
+ "ec2_transitgateway_auto_accept_vpc_attachments",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -1924,7 +1937,10 @@
"kms_cmk_are_used",
"kms_cmk_not_deleted_unintentionally",
"kms_cmk_not_multi_region",
- "kms_cmk_rotation_enabled"
+ "kms_cmk_rotation_enabled",
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_attestation_no_deployment_binding"
]
},
{
diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json
index 7b0446ac3f..78b76a5b9b 100644
--- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json
+++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json
@@ -1647,7 +1647,9 @@
"vpc_peering_routing_tables_with_least_privilege",
"vpc_subnet_no_public_ip_by_default",
"vpc_subnet_separate_private_public",
- "workspaces_vpc_2private_1public_subnets_nat"
+ "workspaces_vpc_2private_1public_subnets_nat",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -1745,7 +1747,9 @@
"sagemaker_notebook_instance_root_access_disabled",
"ses_identity_not_publicly_accessible",
"ssm_documents_set_as_public",
- "vpc_endpoint_connections_trust_boundaries"
+ "vpc_endpoint_connections_trust_boundaries",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -2064,6 +2068,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -2175,7 +2180,8 @@
"secretsmanager_secret_rotated_periodically",
"secretsmanager_secret_unused",
"sns_topics_kms_encryption_at_rest_enabled",
- "storagegateway_fileshare_encryption_enabled"
+ "storagegateway_fileshare_encryption_enabled",
+ "kms_key_enclave_attestation_not_enforced"
],
"Attributes": [
{
@@ -3142,6 +3148,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -3366,7 +3373,10 @@
"wafv2_webacl_with_rules",
"wellarchitected_workload_no_high_or_medium_risks",
"workspaces_volume_encryption_enabled",
- "workspaces_vpc_2private_1public_subnets_nat"
+ "workspaces_vpc_2private_1public_subnets_nat",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"ConfigRequirements": [
{
diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json
index 40b338ce41..668b6d0c21 100644
--- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json
+++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json
@@ -1646,7 +1646,9 @@
"vpc_peering_routing_tables_with_least_privilege",
"vpc_subnet_no_public_ip_by_default",
"vpc_subnet_separate_private_public",
- "workspaces_vpc_2private_1public_subnets_nat"
+ "workspaces_vpc_2private_1public_subnets_nat",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -1745,7 +1747,9 @@
"sagemaker_notebook_instance_root_access_disabled",
"ses_identity_not_publicly_accessible",
"ssm_documents_set_as_public",
- "vpc_endpoint_connections_trust_boundaries"
+ "vpc_endpoint_connections_trust_boundaries",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -2066,6 +2070,7 @@
"elb_ssl_listeners",
"elb_ssl_listeners_use_acm_certificate",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -2177,7 +2182,8 @@
"secretsmanager_secret_rotated_periodically",
"secretsmanager_secret_unused",
"sns_topics_kms_encryption_at_rest_enabled",
- "storagegateway_fileshare_encryption_enabled"
+ "storagegateway_fileshare_encryption_enabled",
+ "kms_key_enclave_attestation_not_enforced"
],
"Attributes": [
{
@@ -3145,6 +3151,7 @@
"elb_ssl_listeners_use_acm_certificate",
"elbv2_desync_mitigation_mode",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -3369,7 +3376,10 @@
"wafv2_webacl_with_rules",
"wellarchitected_workload_no_high_or_medium_risks",
"workspaces_volume_encryption_enabled",
- "workspaces_vpc_2private_1public_subnets_nat"
+ "workspaces_vpc_2private_1public_subnets_nat",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"ConfigRequirements": [
{
diff --git a/prowler/compliance/aws/mitre_attack_aws.json b/prowler/compliance/aws/mitre_attack_aws.json
index 3ac8cf0432..8f4fb581f9 100644
--- a/prowler/compliance/aws/mitre_attack_aws.json
+++ b/prowler/compliance/aws/mitre_attack_aws.json
@@ -33,7 +33,9 @@
"inspector2_is_enabled",
"inspector2_active_findings_exist",
"awslambda_function_not_publicly_accessible",
- "ec2_instance_public_ip"
+ "ec2_instance_public_ip",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
],
"ConfigRequirements": [
{
@@ -224,7 +226,9 @@
"organizations_account_part_of_organizations",
"organizations_delegated_administrators",
"organizations_scp_check_deny_regions",
- "securityhub_enabled"
+ "securityhub_enabled",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_attestation_unknown_image"
],
"ConfigRequirements": [
{
@@ -722,7 +726,8 @@
"securityhub_enabled",
"guardduty_is_enabled",
"inspector2_is_enabled",
- "inspector2_active_findings_exist"
+ "inspector2_active_findings_exist",
+ "kms_key_enclave_debug_attestation_detected"
],
"ConfigRequirements": [
{
@@ -1193,7 +1198,9 @@
"ecs_task_definitions_no_environment_secrets",
"eks_cluster_kms_cmk_encryption_in_secrets_enabled",
"ssm_document_secrets",
- "secretsmanager_automatic_rotation_enabled"
+ "secretsmanager_automatic_rotation_enabled",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_not_enforced"
],
"ConfigRequirements": [
{
@@ -2353,7 +2360,8 @@
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"ConfigRequirements": [
{
diff --git a/prowler/compliance/aws/nis2_aws.json b/prowler/compliance/aws/nis2_aws.json
index 3a4d567d25..995c9a6dac 100644
--- a/prowler/compliance/aws/nis2_aws.json
+++ b/prowler/compliance/aws/nis2_aws.json
@@ -1345,7 +1345,8 @@
"autoscaling_group_launch_configuration_requires_imdsv2",
"ec2_instance_account_imdsv2_enabled",
"ec2_instance_imdsv2_enabled",
- "ec2_launch_template_imdsv2_required"
+ "ec2_launch_template_imdsv2_required",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
],
"Attributes": [
{
diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json
index 921bd33a53..ae7b9998b3 100644
--- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json
+++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json
@@ -552,7 +552,8 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"ssm_managed_compliant_patching",
- "ec2_securitygroup_default_restrict_traffic"
+ "ec2_securitygroup_default_restrict_traffic",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -570,7 +571,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -717,6 +719,7 @@
"apigateway_restapi_client_certificate_enabled",
"ec2_ebs_volume_encryption",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -1057,7 +1060,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -1118,7 +1122,8 @@
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
"sagemaker_notebook_instance_encryption_enabled",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json
index e0ef936229..13a0e0772c 100644
--- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json
+++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json
@@ -337,7 +337,9 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -1102,7 +1104,8 @@
"sagemaker_notebook_instance_without_direct_internet_access_configured",
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -1236,7 +1239,9 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path"
]
},
{
@@ -2970,7 +2975,8 @@
"s3_account_level_public_access_blocks",
"ec2_securitygroup_default_restrict_traffic",
"vpc_flow_logs_enabled",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "kms_key_enclave_attestation_pcr_mismatch"
]
},
{
@@ -2986,7 +2992,8 @@
}
],
"Checks": [
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -3428,7 +3435,8 @@
"rds_instance_backup_enabled",
"rds_instance_multi_az",
"redshift_cluster_automated_snapshot",
- "s3_bucket_object_versioning"
+ "s3_bucket_object_versioning",
+ "ec2_confidential_workload_host_not_running"
]
},
{
@@ -3639,7 +3647,8 @@
"rds_instance_backup_enabled",
"rds_instance_multi_az",
"redshift_cluster_automated_snapshot",
- "s3_bucket_object_versioning"
+ "s3_bucket_object_versioning",
+ "ec2_confidential_workload_host_not_running"
]
},
{
@@ -5007,7 +5016,8 @@
"s3_bucket_policy_public_write_access",
"s3_account_level_public_access_blocks",
"s3_bucket_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -5068,7 +5078,8 @@
"s3_bucket_secure_transport_policy",
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -5187,7 +5198,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -5496,7 +5508,8 @@
"ec2_securitygroup_default_restrict_traffic",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
- "ec2_networkacl_allow_ingress_any_port"
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -5592,6 +5605,7 @@
"Checks": [
"apigateway_restapi_client_certificate_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -5716,7 +5730,10 @@
}
],
"Checks": [
- "kms_cmk_rotation_enabled"
+ "kms_cmk_rotation_enabled",
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_attestation_no_deployment_binding"
]
},
{
@@ -5945,7 +5962,10 @@
"s3_bucket_default_encryption",
"s3_bucket_default_encryption",
"sagemaker_notebook_instance_encryption_enabled",
- "sns_topics_kms_encryption_at_rest_enabled"
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_debug_attestation_detected"
]
},
{
@@ -6405,7 +6425,9 @@
"guardduty_is_enabled",
"rds_instance_integration_cloudwatch_logs",
"redshift_cluster_audit_logging",
- "s3_bucket_server_access_logging_enabled"
+ "s3_bucket_server_access_logging_enabled",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
],
"ConfigRequirements": [
{
@@ -6825,7 +6847,8 @@
}
],
"Checks": [
- "cloudtrail_log_file_validation_enabled"
+ "cloudtrail_log_file_validation_enabled",
+ "kms_key_enclave_attestation_pcr_mismatch"
]
},
{
diff --git a/prowler/compliance/aws/nist_csf_2.0_aws.json b/prowler/compliance/aws/nist_csf_2.0_aws.json
index c06eeec11d..832cb5f637 100644
--- a/prowler/compliance/aws/nist_csf_2.0_aws.json
+++ b/prowler/compliance/aws/nist_csf_2.0_aws.json
@@ -876,7 +876,8 @@
"s3_account_level_public_access_blocks",
"s3_bucket_level_public_access_block",
"s3_bucket_public_access",
- "s3_multi_region_access_point_public_access_block"
+ "s3_multi_region_access_point_public_access_block",
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
]
},
{
@@ -938,7 +939,8 @@
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
@@ -1694,7 +1696,9 @@
"vpc_flow_logs_enabled",
"guardduty_is_enabled",
"inspector2_is_enabled",
- "accessanalyzer_enabled_without_findings"
+ "accessanalyzer_enabled_without_findings",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
],
"ConfigRequirements": [
{
diff --git a/prowler/compliance/aws/pci_3.2.1_aws.json b/prowler/compliance/aws/pci_3.2.1_aws.json
index ca8e968bf9..85fef0b3e5 100644
--- a/prowler/compliance/aws/pci_3.2.1_aws.json
+++ b/prowler/compliance/aws/pci_3.2.1_aws.json
@@ -76,7 +76,8 @@
"s3_bucket_public_write_acl",
"dms_instance_no_public_access",
"sagemaker_notebook_instance_without_direct_internet_access_configured",
- "ec2_networkacl_allow_ingress_tcp_port_3389"
+ "ec2_networkacl_allow_ingress_tcp_port_3389",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -106,7 +107,8 @@
"s3_bucket_public_write_acl",
"dms_instance_no_public_access",
"sagemaker_notebook_instance_without_direct_internet_access_configured",
- "ec2_networkacl_allow_ingress_tcp_port_3389"
+ "ec2_networkacl_allow_ingress_tcp_port_3389",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -294,7 +296,9 @@
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
"ec2_networkacl_allow_ingress_tcp_port_22",
"ec2_networkacl_allow_ingress_tcp_port_3389",
- "ec2_securitygroup_default_restrict_traffic"
+ "ec2_securitygroup_default_restrict_traffic",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress"
],
"Attributes": [
{
@@ -1008,7 +1012,10 @@
"Id": "3.5",
"Name": "Document and implement procedures to protect keys used to secure stored cardholder data against disclosure and misuse",
"Description": "Note: This requirement applies to keys used to encrypt stored cardholder data, and also applies to key-encrypting keys used to protect data-encrypting keys—such key- encrypting keys must be at least as strong as the data-encrypting key. Cryptographic keys must be strongly protected because those who obtain access will be able to decrypt data. Key-encrypting keys, if used, must be at least as strong as the data-encrypting key in order to ensure proper protection of the key that encrypts the data as well as the data encrypted with that key. The requirement to protect keys from disclosure and misuse applies to both data-encrypting keys and key-encrypting keys. Because one key- encrypting key may grant access to many data- encrypting keys, the key-encrypting keys require strong protection measures.",
- "Checks": [],
+ "Checks": [
+ "kms_key_enclave_attestation_not_enforced",
+ "kms_key_enclave_attestation_bypassable_path"
+ ],
"Attributes": [
{
"ItemId": "3.5",
@@ -1034,7 +1041,9 @@
"Id": "3.6",
"Name": "Fully document and implement all key-management processes and procedures for cryptographic keys used for encryption of cardholder data",
"Description": "Note: Numerous industry standards for key management are available from various resources including NIST, which can be found at http://csrc.nist.gov. The manner in which cryptographic keys are managed is a critical part of the continued security of the encryption solution. A good key- management process, whether it is manual or automated as part of the encryption product, is based on industry standards and addresses all key elements at 3.6.1 through 3.6.8. Providing guidance to customers on how to securely transmit, store and update cryptographic keys can help prevent keys from being mismanaged or disclosed to unauthorized entities. This requirement applies to keys used to encrypt stored cardholder data, and any respective key- encrypting keys. Note: Testing Procedure 3.6.a is an additional procedure that only applies if the entity being assessed is a service provider.",
- "Checks": [],
+ "Checks": [
+ "kms_key_enclave_attestation_not_enforced"
+ ],
"Attributes": [
{
"ItemId": "3.6",
@@ -1500,7 +1509,9 @@
"s3_bucket_policy_public_write_access",
"s3_bucket_public_write_acl",
"dms_instance_no_public_access",
- "sagemaker_notebook_instance_without_direct_internet_access_configured"
+ "sagemaker_notebook_instance_without_direct_internet_access_configured",
+ "ec2_confidential_workload_host_public_ip",
+ "kms_key_enclave_attestation_bypassable_path"
],
"Attributes": [
{
@@ -2131,7 +2142,8 @@
"cloudtrail_s3_dataevents_read_enabled",
"cloudtrail_multi_region_enabled",
"cloudtrail_cloudwatch_logging_enabled",
- "redshift_cluster_audit_logging"
+ "redshift_cluster_audit_logging",
+ "kms_key_enclave_debug_attestation_detected"
],
"Attributes": [
{
diff --git a/prowler/compliance/aws/pci_4.0_aws.json b/prowler/compliance/aws/pci_4.0_aws.json
index e21b543556..b750d3784a 100644
--- a/prowler/compliance/aws/pci_4.0_aws.json
+++ b/prowler/compliance/aws/pci_4.0_aws.json
@@ -1603,6 +1603,20 @@
}
]
},
+ {
+ "Id": "1.3.1.53",
+ "Description": "Checks if Nitro Enclave parent instances have a public IP address or reside in a subnet routed to an internet gateway",
+ "Name": "ec2",
+ "Checks": [
+ "ec2_confidential_workload_host_public_ip"
+ ],
+ "Attributes": [
+ {
+ "Section": "1.3.1: Network access to and from the cardholder data environment is restricted. ",
+ "Service": "ec2"
+ }
+ ]
+ },
{
"Id": "1.3.2.1",
"Description": "Checks if an Amazon API Gateway API stage is using an AWS WAF web access control list (web ACL)",
@@ -2318,6 +2332,20 @@
}
]
},
+ {
+ "Id": "1.3.2.53",
+ "Description": "Checks if security groups attached to Nitro Enclave parent instances allow unrestricted ingress from 0.0.0.0/0 or ::/0",
+ "Name": "ec2",
+ "Checks": [
+ "ec2_confidential_workload_host_unrestricted_ingress"
+ ],
+ "Attributes": [
+ {
+ "Section": "1.3.2: Network access to and from the cardholder data environment is restricted. ",
+ "Service": "ec2"
+ }
+ ]
+ },
{
"Id": "1.4.1.1",
"Description": "Checks if an Amazon API Gateway API stage is using an AWS WAF web access control list (web ACL)",
@@ -3139,6 +3167,20 @@
}
]
},
+ {
+ "Id": "1.4.2.51",
+ "Description": "Checks if security groups attached to Nitro Enclave parent instances expose vsock-proxy TCP ports to sources outside the VPC",
+ "Name": "ec2",
+ "Checks": [
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
+ ],
+ "Attributes": [
+ {
+ "Section": "1.4.2: Network connections between trusted and untrusted networks are controlled. ",
+ "Service": "ec2"
+ }
+ ]
+ },
{
"Id": "1.4.3.1",
"Description": "Checks if an AWS Network Firewall policy is configured with a user defined stateless default action for fragmented packets",
@@ -9420,6 +9462,20 @@
}
]
},
+ {
+ "Id": "10.4.1.7",
+ "Description": "Checks if KMS attestation activity originates from enclave images whose PCR values do not match any known record",
+ "Name": "kms",
+ "Checks": [
+ "kms_key_enclave_attestation_unknown_image"
+ ],
+ "Attributes": [
+ {
+ "Section": "10.4.1: Audit logs are reviewed to identify anomalies or suspicious activity. ",
+ "Service": "kms"
+ }
+ ]
+ },
{
"Id": "10.4.2.1",
"Description": "Checks if AWS X-Ray tracing is enabled on Amazon API Gateway REST APIs",
@@ -11230,6 +11286,20 @@
}
]
},
+ {
+ "Id": "2.2.5.18",
+ "Description": "Checks if Nitro Enclave parent instances enforce IMDSv2 by requiring session tokens for instance metadata requests",
+ "Name": "ec2",
+ "Checks": [
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
+ ],
+ "Attributes": [
+ {
+ "Section": "2.2.5: System components are configured and managed securely. ",
+ "Service": "ec2"
+ }
+ ]
+ },
{
"Id": "2.2.7.1",
"Description": "Checks if HTTP to HTTPS redirection is configured on all HTTP listeners of Application Load Balancers",
@@ -13184,6 +13254,20 @@
}
]
},
+ {
+ "Id": "3.5.1.36",
+ "Description": "Checks if KMS attestation events record enclaves running in debug mode, identified by all-zero PCR values",
+ "Name": "kms",
+ "Checks": [
+ "kms_key_enclave_debug_attestation_detected"
+ ],
+ "Attributes": [
+ {
+ "Section": "3.5.1: Primary account number (PAN) is secured wherever it is stored. ",
+ "Service": "kms"
+ }
+ ]
+ },
{
"Id": "3.6.1.2.1",
"Description": "Checks if AWS Certificate Manager Certificates in your account are marked for expiration within the specified number of days",
@@ -13672,6 +13756,20 @@
}
]
},
+ {
+ "Id": "3.6.1.10",
+ "Description": "Checks if KMS key policies used by Nitro Enclave workloads require enclave attestation condition keys",
+ "Name": "kms",
+ "Checks": [
+ "kms_key_enclave_attestation_not_enforced"
+ ],
+ "Attributes": [
+ {
+ "Section": "3.6.1: Cryptographic keys used to protect stored account data are secured. ",
+ "Service": "kms"
+ }
+ ]
+ },
{
"Id": "3.7.1.1",
"Description": "Checks if AWS Certificate Manager Certificates in your account are marked for expiration within the specified number of days",
@@ -13819,6 +13917,20 @@
}
]
},
+ {
+ "Id": "3.7.1.11",
+ "Description": "Checks if the PCR values authorized in KMS key policies match the customer-maintained golden values for expected enclave images",
+ "Name": "kms",
+ "Checks": [
+ "kms_key_enclave_attestation_pcr_mismatch"
+ ],
+ "Attributes": [
+ {
+ "Section": "3.7.1: Where cryptography is used to protect stored account data, key management processes and procedures covering all aspects of the key lifecycle are defined and implemented. ",
+ "Service": "kms"
+ }
+ ]
+ },
{
"Id": "3.7.2.1",
"Description": "Checks if AWS Certificate Manager Certificates in your account are marked for expiration within the specified number of days",
@@ -16481,6 +16593,20 @@
}
]
},
+ {
+ "Id": "7.2.1.30",
+ "Description": "Checks if KMS key policies contain an alternative authorization path granting the same operations without requiring enclave attestation",
+ "Name": "kms",
+ "Checks": [
+ "kms_key_enclave_attestation_bypassable_path"
+ ],
+ "Attributes": [
+ {
+ "Section": "7.2.1: Access to system components and data is appropriately defined and assigned. ",
+ "Service": "kms"
+ }
+ ]
+ },
{
"Id": "7.2.2.1",
"Description": "Checks if an AWS account is part of AWS Organizations",
@@ -19373,6 +19499,20 @@
}
]
},
+ {
+ "Id": "8.2.8.25",
+ "Description": "Checks if Nitro Enclave parent instances enforce IMDSv2, preventing unauthenticated retrieval of the credentials used to call KMS",
+ "Name": "ec2",
+ "Checks": [
+ "ec2_confidential_workload_host_imdsv2_not_enforced"
+ ],
+ "Attributes": [
+ {
+ "Section": "8.2.8: User identification and related accounts for users and administrators are strictly managed throughout an accounts lifecycle. ",
+ "Service": "ec2"
+ }
+ ]
+ },
{
"Id": "8.3.10.1.1",
"Description": "Checks if active IAM access keys are rotated (changed) within the number of days specified in maxAccessKeyAge",
diff --git a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json
index 5de1f5ca8a..554b40cdad 100644
--- a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json
+++ b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json
@@ -40,6 +40,7 @@
"ec2_instance_public_ip",
"efs_encryption_at_rest_enabled",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json
index 701f931b05..8d5896d3cd 100644
--- a/prowler/compliance/aws/secnumcloud_3.2_aws.json
+++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json
@@ -428,7 +428,8 @@
"s3_account_level_public_access_blocks",
"s3_bucket_level_public_access_block",
"rds_instance_no_public_access",
- "ec2_instance_public_ip"
+ "ec2_instance_public_ip",
+ "ec2_confidential_workload_host_public_ip"
]
},
{
@@ -490,6 +491,7 @@
"elbv2_ssl_listeners",
"elb_insecure_ssl_ciphers",
"elbv2_insecure_ssl_ciphers",
+ "elbv2_listener_pqc_tls_enabled",
"cloudfront_distributions_pqc_tls_enabled",
"apigateway_domain_name_pqc_tls_enabled",
"transfer_server_pqc_ssh_kex_enabled",
@@ -561,7 +563,8 @@
"ecs_task_definitions_no_environment_secrets",
"codebuild_project_no_secrets_in_variables",
"ssm_document_secrets",
- "cloudwatch_log_group_no_secrets_in_logs"
+ "cloudwatch_log_group_no_secrets_in_logs",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -1107,7 +1110,8 @@
"vpc_endpoint_connections_trust_boundaries",
"vpc_endpoint_services_allowed_principals_trust_boundaries",
"elbv2_waf_acl_attached",
- "wafv2_webacl_with_rules"
+ "wafv2_webacl_with_rules",
+ "ec2_confidential_workload_host_unrestricted_ingress"
]
},
{
diff --git a/prowler/compliance/aws/soc2_aws.json b/prowler/compliance/aws/soc2_aws.json
index c0041a9dae..0e8c9f70ae 100644
--- a/prowler/compliance/aws/soc2_aws.json
+++ b/prowler/compliance/aws/soc2_aws.json
@@ -225,7 +225,8 @@
}
],
"Checks": [
- "s3_bucket_public_access"
+ "s3_bucket_public_access",
+ "kms_key_enclave_attestation_not_enforced"
]
},
{
@@ -262,7 +263,9 @@
"bedrock_full_access_policy_attached",
"iam_aws_attached_policy_no_administrative_privileges",
"iam_customer_attached_policy_no_administrative_privileges",
- "iam_inline_policy_no_administrative_privileges"
+ "iam_inline_policy_no_administrative_privileges",
+ "kms_key_enclave_attestation_bypassable_path",
+ "kms_key_enclave_attestation_no_deployment_binding"
]
},
{
@@ -322,7 +325,10 @@
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379",
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
+ "ec2_confidential_workload_host_public_ip",
+ "ec2_confidential_workload_host_unrestricted_ingress",
+ "ec2_confidential_workload_host_vsock_proxy_exposed"
]
},
{
@@ -396,7 +402,8 @@
"guardduty_is_enabled",
"securityhub_enabled",
"ec2_instance_managed_by_ssm",
- "ssm_managed_compliant_patching"
+ "ssm_managed_compliant_patching",
+ "kms_key_enclave_attestation_pcr_mismatch"
],
"ConfigRequirements": [
{
@@ -446,7 +453,10 @@
"ec2_instance_imdsv2_enabled",
"guardduty_is_enabled",
"apigateway_restapi_logging_enabled",
- "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22"
+ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
+ "ec2_confidential_workload_host_imdsv2_not_enforced",
+ "kms_key_enclave_debug_attestation_detected",
+ "kms_key_enclave_attestation_unknown_image"
],
"ConfigRequirements": [
{
@@ -645,7 +655,8 @@
"stepfunctions_statemachine_logging_enabled",
"waf_global_webacl_logging_enabled",
"wafv2_webacl_logging_enabled",
- "wafv2_webacl_rule_logging_enabled"
+ "wafv2_webacl_rule_logging_enabled",
+ "ec2_confidential_workload_host_not_running"
]
},
{
diff --git a/tests/__init__.py b/prowler/compliance/huaweicloud/__init__.py
similarity index 100%
rename from tests/__init__.py
rename to prowler/compliance/huaweicloud/__init__.py
diff --git a/prowler/compliance/huaweicloud/cis_1.0_huaweicloud.json b/prowler/compliance/huaweicloud/cis_1.0_huaweicloud.json
new file mode 100644
index 0000000000..8559a06f75
--- /dev/null
+++ b/prowler/compliance/huaweicloud/cis_1.0_huaweicloud.json
@@ -0,0 +1,429 @@
+{
+ "Framework": "CIS",
+ "Name": "CIS Huawei Cloud Foundations Benchmark v1.0.0",
+ "Version": "1.0",
+ "Provider": "HuaweiCloud",
+ "Description": "CIS Huawei Cloud Foundations Benchmark v1.0.0 provides prescriptive guidance for configuring security options for a subset of Huawei Cloud services. It has been developed to help cloud operators establish a secure baseline configuration for their Huawei Cloud environment.",
+ "Requirements": [
+ {
+ "Id": "1.1",
+ "Description": "Ensure IAM password policy requires minimum password length of 14 or greater",
+ "Checks": [
+ "iam_account_password_policy"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure IAM password policy requires a minimum password length of 14 or greater characters.",
+ "RationaleStatement": "Short passwords are easier to crack via brute force attacks. A minimum length of 14 characters significantly increases the keyspace and provides exponentially more security against automated password cracking.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Choose IAM & Security. 3. Click the Password Policy tab. 4. Set Minimum Password Length to 14 or greater. 5. Click OK.",
+ "AuditProcedure": "Run prowler to check the IAM password policy minimum length configuration.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0060.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.2",
+ "Description": "Ensure IAM password policy requires passwords to expire",
+ "Checks": [
+ "iam_password_policy_expires_passwords"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure IAM password policy requires passwords to expire after a defined period.",
+ "RationaleStatement": "Regular password expiration reduces the risk of compromised credentials being used indefinitely. It forces users to periodically update their passwords, limiting the window of opportunity for attackers.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Choose IAM & Security. 3. Click the Password Policy tab. 4. Set Password Validity Period to a non-zero value (e.g., 90 days). 5. Click OK.",
+ "AuditProcedure": "Run prowler to check if the IAM password policy has a password validity period set.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0060.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.3",
+ "Description": "Ensure IAM password policy prevents password reuse",
+ "Checks": [
+ "iam_password_policy_reuse_prevention"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure IAM password policy disallows reuse of at least the last 3 passwords.",
+ "RationaleStatement": "Preventing password reuse ensures users cannot cycle through previously used passwords, which reduces the risk of compromised credentials being reused.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Choose IAM & Security. 3. Click the Password Policy tab. 4. Set Number of Recent Passwords Disallowed to 3 or greater. 5. Click OK.",
+ "AuditProcedure": "Run prowler to check if the IAM password policy disallows reuse of at least 3 recent passwords.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0060.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.4",
+ "Description": "Ensure IAM password policy requires character combination",
+ "Checks": [
+ "iam_password_policy_char_combination"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure IAM password policy requires at least 3 character types (uppercase, lowercase, digits, special characters).",
+ "RationaleStatement": "Requiring multiple character types increases password complexity and makes passwords more resistant to dictionary and brute force attacks.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Choose IAM & Security. 3. Click the Password Policy tab. 4. Set Password Character Combination to 3 or greater. 5. Click OK.",
+ "AuditProcedure": "Run prowler to check if the IAM password policy requires at least 3 character types.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0060.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.5",
+ "Description": "Ensure IAM password policy enforces minimum password age",
+ "Checks": [
+ "iam_password_policy_minimum_age"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure IAM password policy enforces a minimum password age to prevent users from changing passwords too frequently.",
+ "RationaleStatement": "A minimum password age prevents users from rapidly cycling through passwords to bypass password reuse restrictions.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Choose IAM & Security. 3. Click the Password Policy tab. 4. Set Minimum Password Age to a non-zero value (e.g., 1 day). 5. Click OK.",
+ "AuditProcedure": "Run prowler to check if the IAM password policy enforces a minimum password age.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0060.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.6",
+ "Description": "Ensure root account has hardware MFA enabled",
+ "Checks": [
+ "iam_root_hardware_mfa_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure the root account has hardware multi-factor authentication (MFA) enabled.",
+ "RationaleStatement": "The root account is the most privileged account in the Huawei Cloud environment. Enabling hardware MFA provides an additional layer of security against unauthorized access.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console as root. 2. Go to IAM & Security. 3. Click the MFA tab. 4. Enable virtual or hardware MFA for the root account. 5. Follow the setup instructions.",
+ "AuditProcedure": "Run prowler to check if the root account has hardware MFA enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0012.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.7",
+ "Description": "Ensure all IAM users have MFA enabled",
+ "Checks": [
+ "iam_user_mfa_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure all IAM users have multi-factor authentication (MFA) enabled.",
+ "RationaleStatement": "MFA provides an additional layer of security against unauthorized access. Without MFA, a compromised password alone is sufficient to gain access.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to IAM & Security > Users. 3. For each user, click Enable MFA and follow the setup instructions.",
+ "AuditProcedure": "Run prowler to check if all IAM users have MFA enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0012.html"
+ }
+ ]
+ },
+ {
+ "Id": "1.8",
+ "Description": "Ensure disabled IAM users are reviewed",
+ "Checks": [
+ "iam_user_disabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "1 Identity and Access Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure disabled IAM users are identified and reviewed for potential removal.",
+ "RationaleStatement": "Disabled user accounts may retain permissions and could be re-enabled by an attacker. Regular review ensures stale accounts are removed.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to IAM & Security > Users. 3. Review disabled users. 4. Remove accounts that are no longer needed.",
+ "AuditProcedure": "Run prowler to identify disabled IAM users.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_01_0033.html"
+ }
+ ]
+ },
+ {
+ "Id": "2.1",
+ "Description": "Ensure OBS buckets are not publicly accessible",
+ "Checks": [
+ "obs_bucket_public_access"
+ ],
+ "Attributes": [
+ {
+ "Section": "2 Storage",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure OBS buckets do not allow public read or write access.",
+ "RationaleStatement": "Publicly accessible buckets expose data to anyone on the internet, which can lead to data breaches and unauthorized access.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Object Storage Service. 3. Select each bucket. 4. Review and modify the bucket ACL to remove public access. 5. Click OK.",
+ "AuditProcedure": "Run prowler to check if any OBS buckets are publicly accessible.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-obs/obs_03_0113.html"
+ }
+ ]
+ },
+ {
+ "Id": "2.2",
+ "Description": "Ensure EVS volumes have encryption enabled",
+ "Checks": [
+ "evs_volume_encryption"
+ ],
+ "Attributes": [
+ {
+ "Section": "2 Storage",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure all EVS volumes have encryption enabled.",
+ "RationaleStatement": "Encrypting EVS volumes protects data at rest against unauthorized access if the physical storage media is compromised.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Elastic Volume Service. 3. For each unencrypted volume, create an encrypted volume and migrate data. 4. Delete the unencrypted volume.",
+ "AuditProcedure": "Run prowler to check if all EVS volumes have encryption enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-evs/evs_01_0044.html"
+ }
+ ]
+ },
+ {
+ "Id": "3.1",
+ "Description": "Ensure default security groups restrict all traffic",
+ "Checks": [
+ "vpc_default_security_group_restricts_all_traffic"
+ ],
+ "Attributes": [
+ {
+ "Section": "3 Network Security",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure default security groups do not allow unrestricted inbound or outbound traffic.",
+ "RationaleStatement": "Default security groups with open rules expose resources to traffic from any source, increasing the attack surface.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to VPC > Security Groups. 3. Select the default security group. 4. Remove any rules with 0.0.0.0/0 or ::/0 as source/destination. 5. Add restrictive rules as needed.",
+ "AuditProcedure": "Run prowler to check if default security groups restrict all traffic.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/vpc_faq/vpc_faq_0036.html"
+ }
+ ]
+ },
+ {
+ "Id": "3.2",
+ "Description": "Ensure security groups do not allow open ingress on sensitive ports",
+ "Checks": [
+ "vpc_security_group_open_ingress"
+ ],
+ "Attributes": [
+ {
+ "Section": "3 Network Security",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure security groups do not allow open ingress (0.0.0.0/0) on sensitive ports (SSH, RDP, MySQL, Redis, MongoDB).",
+ "RationaleStatement": "Exposing sensitive ports to the internet allows attackers to attempt brute force attacks, exploitation, or unauthorized access to services.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to VPC > Security Groups. 3. For each security group, review ingress rules. 4. Remove or restrict rules that allow 0.0.0.0/0 on sensitive ports. 5. Use bastion host or VPN for access instead.",
+ "AuditProcedure": "Run prowler to check if any security groups allow open ingress on sensitive ports.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/vpc_faq/vpc_faq_0036.html"
+ }
+ ]
+ },
+ {
+ "Id": "3.3",
+ "Description": "Ensure WAF is enabled",
+ "Checks": [
+ "waf_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "3 Network Security",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure Web Application Firewall (WAF) is enabled to protect web applications from common attacks.",
+ "RationaleStatement": "WAF protects web applications from common web exploits such as SQL injection, XSS, and CSRF attacks.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Web Application Firewall. 3. Create or configure a WAF policy. 4. Enable WAF for your web applications.",
+ "AuditProcedure": "Run prowler to check if WAF is enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-waf/waf_01_0001.html"
+ }
+ ]
+ },
+ {
+ "Id": "4.1",
+ "Description": "Ensure ECS instances do not have public IP addresses",
+ "Checks": [
+ "ecs_instance_public_ip"
+ ],
+ "Attributes": [
+ {
+ "Section": "4 Compute",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure ECS instances do not have public IP addresses unless required.",
+ "RationaleStatement": "Public IP addresses expose instances to the internet, increasing the attack surface. Use a bastion host or VPN for access instead.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Elastic Cloud Server. 3. For each instance with a public IP, release the EIP if not required. 4. Use a bastion host or VPN for access.",
+ "AuditProcedure": "Run prowler to check if any ECS instances have public IP addresses.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-ecs/ecs_03_0304.html"
+ }
+ ]
+ },
+ {
+ "Id": "5.1",
+ "Description": "Ensure RDS instances have backup enabled",
+ "Checks": [
+ "rds_backup_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Database",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure all RDS instances have automated backup enabled.",
+ "RationaleStatement": "Automated backups ensure data can be recovered in case of data loss, corruption, or disaster.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Relational Database Service. 3. For each instance, enable automated backup. 4. Configure backup retention period.",
+ "AuditProcedure": "Run prowler to check if all RDS instances have backup enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-rds/rds_05_0037.html"
+ }
+ ]
+ },
+ {
+ "Id": "5.2",
+ "Description": "Ensure RDS instances are not publicly accessible",
+ "Checks": [
+ "rds_public_access"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Database",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure RDS instances are not publicly accessible.",
+ "RationaleStatement": "Publicly accessible databases expose data to anyone on the internet, significantly increasing the risk of unauthorized access and data breaches.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Relational Database Service. 3. For each instance, remove the public IP address. 4. Configure VPC-only access.",
+ "AuditProcedure": "Run prowler to check if any RDS instances are publicly accessible.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-rds/rds_03_0077.html"
+ }
+ ]
+ },
+ {
+ "Id": "6.1",
+ "Description": "Ensure ELB load balancers are not publicly exposed",
+ "Checks": [
+ "elb_public_exposure"
+ ],
+ "Attributes": [
+ {
+ "Section": "6 Load Balancing",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure ELB load balancers are not publicly exposed unless required.",
+ "RationaleStatement": "Publicly exposed load balancers can be targeted by DDoS attacks and unauthorized access attempts.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Elastic Load Balance. 3. Review each load balancer. 4. For internal-facing services, switch to internal load balancer.",
+ "AuditProcedure": "Run prowler to check if any ELB load balancers are publicly exposed.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-elb/elb_01_0001.html"
+ }
+ ]
+ },
+ {
+ "Id": "7.1",
+ "Description": "Ensure CTS tracking is enabled",
+ "Checks": [
+ "cts_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "7 Logging and Monitoring",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure Cloud Trace Service (CTS) tracking is enabled for auditing and compliance.",
+ "RationaleStatement": "CTS tracking records all API calls and configuration changes, providing an audit trail for security analysis and compliance.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Cloud Trace Service. 3. Create or enable a tracker. 4. Configure the tracker to record all management and data events.",
+ "AuditProcedure": "Run prowler to check if CTS tracking is enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-cts/cts_01_0003.html"
+ }
+ ]
+ },
+ {
+ "Id": "8.1",
+ "Description": "Ensure KMS keys have rotation enabled",
+ "Checks": [
+ "kms_key_rotation_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "8 Key Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure all KMS keys have automatic key rotation enabled.",
+ "RationaleStatement": "Key rotation regularly replaces cryptographic material, reducing the risk of key compromise over time.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Key Management Service. 3. Select each key. 4. Click the Rotation tab. 5. Enable rotation and set the rotation period.",
+ "AuditProcedure": "Run prowler to check if all KMS keys have rotation enabled.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-kms/kms_01_0019.html"
+ }
+ ]
+ },
+ {
+ "Id": "8.2",
+ "Description": "Ensure KMS keys are not in pending deletion state",
+ "Checks": [
+ "kms_key_not_pending_deletion"
+ ],
+ "Attributes": [
+ {
+ "Section": "8 Key Management",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Ensure KMS keys are not in pending deletion state, which could lead to data loss.",
+ "RationaleStatement": "Keys pending deletion will be permanently deleted after the waiting period, making all data encrypted with those keys unrecoverable.",
+ "ImpactStatement": "",
+ "RemediationProcedure": "1. Log on to the Huawei Cloud console. 2. Go to Key Management Service. 3. Review keys in pending deletion state. 4. Cancel deletion for keys that are still needed.",
+ "AuditProcedure": "Run prowler to check if any KMS keys are in pending deletion state.",
+ "AdditionalInformation": "",
+ "References": "https://support.huaweicloud.com/intl/en-us/usermanual-kms/kms_01_0018.html"
+ }
+ ]
+ }
+ ]
+}
diff --git a/prowler/compliance/m365/cis_7.0_m365.json b/prowler/compliance/m365/cis_7.0_m365.json
index a913f339be..63056dcc22 100644
--- a/prowler/compliance/m365/cis_7.0_m365.json
+++ b/prowler/compliance/m365/cis_7.0_m365.json
@@ -323,7 +323,9 @@
{
"Id": "1.3.9",
"Description": "Shared Bookings allows you to invite your team members and create booking pages and let your customers book time with you and your team. It contains various settings to define services, manage staff members, configure schedules and availability, business hours and customize how appointments are scheduled. These pages can be customized to fit the diverse needs of your organization. It is an extension of Person Bookings. The recommended state is to restrict the OwaMailboxPolicy-Default policy or disable at the organization level.",
- "Checks": [],
+ "Checks": [
+ "admincenter_shared_bookings_disabled"
+ ],
"Attributes": [
{
"Section": "1 Microsoft 365 admin center",
@@ -768,7 +770,9 @@
{
"Id": "2.4.1",
"Description": "Identify priority accounts to utilize Microsoft 365's advanced custom security features. This is an essential tool to bolster protection for users who are frequently targeted due to their critical positions, such as executives, leaders, managers, or others who have access to sensitive, confidential, financial, or high-priority information. Once these accounts are identified, several services and features can be enabled, including threat policies, enhanced sign-in protection through conditional access policies, and alert policies, enabling faster response times for incident response teams.",
- "Checks": [],
+ "Checks": [
+ "defender_priority_account_protection_enabled"
+ ],
"Attributes": [
{
"Section": "2 Microsoft Defender",
@@ -789,7 +793,9 @@
{
"Id": "2.4.2",
"Description": "Preset security policies have been established by Microsoft, utilizing observations and experiences within datacenters to strike a balance between the exclusion of malicious content from users and limiting unwarranted disruptions. These policies can apply to all, or select users and encompass recommendations for addressing spam, malware, and phishing threats. The policy parameters are pre-determined and non-adjustable. Strict protection has the most aggressive protection of the 3 presets. - EOP: Anti-spam, Anti-malware and Anti-phishing - Defender: Spoof protection, Impersonation protection and Advanced phishing - Defender: Safe Links and Safe Attachments NOTE: The preset security polices cannot target Priority account TAGS currently, groups should be used instead.",
- "Checks": [],
+ "Checks": [
+ "defender_strict_preset_security_policy_enabled"
+ ],
"Attributes": [
{
"Section": "2 Microsoft Defender",
@@ -1160,7 +1166,9 @@
{
"Id": "5.1.3.1",
"Description": "This setting allows users in the organization to create new security groups and add members to these groups in the Azure portal, API, or PowerShell. These new groups also show up in the Access Panel for all other users. If the policy setting on the group allows it, other users can create requests to join these groups. The recommended state is Users can create security groups in Azure portals, API or PowerShell set to No.",
- "Checks": [],
+ "Checks": [
+ "entra_policy_default_user_cannot_create_security_groups"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1223,7 +1231,9 @@
{
"Id": "5.1.3.4",
"Description": "All users within a Microsoft Entra organization are permitted to create new Microsoft 365 groups and add members to those groups through the Azure portal, API, or PowerShell. Newly created groups also appear in the Access Panel for all other users. When the applicable group policy settings allow it, users can submit requests to join these groups. The recommended state is No.",
- "Checks": [],
+ "Checks": [
+ "entra_policy_default_user_cannot_create_m365_groups"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1244,7 +1254,9 @@
{
"Id": "5.1.4.1",
"Description": "This setting enables you to select the users who can register their devices as Microsoft Entra joined devices. The recommended state is Selected or None. Note: This setting is applicable only to Microsoft Entra join on Windows 10 or newer. This setting doesn't apply to Microsoft Entra hybrid joined devices, Microsoft Entra joined VMs in Azure, or Microsoft Entra joined devices that use Windows Autopilot self- deployment mode because these methods work in a userless context.",
- "Checks": [],
+ "Checks": [
+ "entra_device_registration_join_restricted"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1265,7 +1277,9 @@
{
"Id": "5.1.4.2",
"Description": "This setting defines the maximum number of Microsoft Entra joined or registered devices that a user can have in Microsoft Entra ID. Once this limit is reached, no additional devices can be added until existing ones are removed. Values above 100 are automatically capped at 100. The recommended state is 10 or less.",
- "Checks": [],
+ "Checks": [
+ "entra_device_registration_max_devices_per_user_limited"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1286,7 +1300,9 @@
{
"Id": "5.1.4.3",
"Description": "This setting controls whether the Global Administrator role is automatically added to the local administrators group on a device during the Microsoft Entra join process. The recommended state is No.",
- "Checks": [],
+ "Checks": [
+ "entra_device_registration_global_admins_not_local_admins"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1307,7 +1323,9 @@
{
"Id": "5.1.4.4",
"Description": "This setting determines if the Microsoft Entra user registering their device as Microsoft Entra join will be added to the local administrators group. This setting applies only once during the actual registration of the device as Microsoft Entra join. The recommended state is Selected or None.",
- "Checks": [],
+ "Checks": [
+ "entra_device_registration_registering_user_not_local_admin"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1328,7 +1346,9 @@
{
"Id": "5.1.4.5",
"Description": "Local Administrator Password Solution (LAPS) is the management of local account passwords on Windows devices. LAPS provides a solution to securely manage and retrieve the built-in local admin password. With cloud version of LAPS, customers can enable storing and rotation of local admin passwords for both Microsoft Entra and Microsoft Entra hybrid join devices The recommended state is Yes.",
- "Checks": [],
+ "Checks": [
+ "entra_device_registration_laps_enabled"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1349,7 +1369,9 @@
{
"Id": "5.1.4.6",
"Description": "This setting determines if users can self-service recover their BitLocker key(s). 'Yes' restricts non-admin users from being able to see the BitLocker key(s) for their owned devices if there are any. 'No' allows all users to recover their BitLocker key(s). The recommended state is Yes.",
- "Checks": [],
+ "Checks": [
+ "entra_policy_default_user_cannot_read_bitlocker_keys"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1508,7 +1530,9 @@
{
"Id": "5.1.6.1",
"Description": "B2B collaboration is a feature within Microsoft Entra External ID that allows for guest invitations to an organization. Ensure users can only send invitations to specified domains. Note: This list works independently from OneDrive for Business and SharePoint Online allow/block lists. To restrict individual file sharing in SharePoint Online, set up an allow or blocklist for OneDrive for Business and SharePoint Online. For instance, in SharePoint or OneDrive users can still share with external users from prohibited domains by using Anyone links if they haven't been disabled.",
- "Checks": [],
+ "Checks": [
+ "entra_policy_guest_invitations_restricted_to_allowed_domains"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -1998,7 +2022,9 @@
{
"Id": "5.2.3.2",
"Description": "With Entra Password Protection, default global banned password lists are automatically applied to all users in an Entra ID tenant. To support business and security needs, custom banned password lists can be defined. When users change or reset their passwords, these banned password lists are checked to enforce the use of strong passwords. A custom banned password list should include some of the following examples: - Brand names - Product names - Locations, such as company headquarters - Company-specific internal terms - Abbreviations that have specific company meaning",
- "Checks": [],
+ "Checks": [
+ "entra_password_protection_custom_banned_list_enforced"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2019,7 +2045,9 @@
{
"Id": "5.2.3.3",
"Description": "Microsoft Entra Password Protection provides a global and custom banned password list. A password change request fails if there's a match in these banned password list. To protect on-premises Active Directory Domain Services (AD DS) environment, install and configure Entra Password Protection. Note: This recommendation applies to Hybrid deployments only and will have no impact unless working with on-premises Active Directory.",
- "Checks": [],
+ "Checks": [
+ "entra_password_protection_on_premises_enforced"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2128,7 +2156,9 @@
{
"Id": "5.2.3.8",
"Description": "The account lockout threshold determines how many failed login attempts are permitted prior to placing the account in a locked-out state and initiating a variable lockout duration. The recommended Lockout threshold is 10 or less.",
- "Checks": [],
+ "Checks": [
+ "entra_password_protection_lockout_threshold_limited"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2149,7 +2179,9 @@
{
"Id": "5.2.3.9",
"Description": "The account lockout duration value determines how long an account retains the status of lockout, and therefore how long before a user can continue to attempt to login after passing the lockout threshold. The recommended state is Lockout duration in seconds is at least 60.",
- "Checks": [],
+ "Checks": [
+ "entra_password_protection_lockout_duration_configured"
+ ],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",
@@ -2571,7 +2603,9 @@
{
"Id": "6.3.2",
"Description": "Outlook on the web (OWA) mailbox policies include two settings that control personal account integration in Outlook. PersonalAccountsEnabled controls whether users can add personal email accounts (e.g., Outlook.com, Gmail, Yahoo) in the new Outlook for Windows. PersonalAccountCalendarsEnabled controls whether users can connect personal Outlook.com or Google calendars in Outlook on the web. Neither setting applies to classic Outlook for Windows, Outlook for Mac, or Outlook mobile apps. The recommended state for the default OWA Mailbox Policy is: - PersonalAccountsEnabled is set to False - PersonalAccountCalendarsEnabled is set to False",
- "Checks": [],
+ "Checks": [
+ "exchange_owa_mailbox_policy_personal_accounts_disabled"
+ ],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -2692,7 +2726,9 @@
{
"Id": "6.5.5",
"Description": "Direct Send is a method used to send emails directly to an Exchange Online customer's hosted mailboxes from on-premises devices, applications, or third-party cloud services using the customer's own accepted domain. This method does not require any form of authentication because, by its nature, it mimics incoming anonymous emails from the internet, apart from the sender domain. The recommended state is to configure RejectDirectSend to True.",
- "Checks": [],
+ "Checks": [
+ "exchange_organization_reject_direct_send_enabled"
+ ],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -3088,7 +3124,9 @@
{
"Id": "8.2.4",
"Description": "This setting controls the organization's external access with Teams \"trial-only\" tenants. These are tenants that don't have any purchased seats. When set to Blocked, users from these trial-only tenants aren't able to search and contact your users via chats, Teams calls, and meetings (using the users' authenticated identities) and your users aren't able to reach users in these trial-only tenants. Users from the trial-only tenant are also removed from existing chats. The recommended state for People in my organization can communicate with accounts in trial Teams tenant is Off.",
- "Checks": [],
+ "Checks": [
+ "teams_external_access_trial_tenants_blocked"
+ ],
"Attributes": [
{
"Section": "8 Microsoft Teams admin center",
@@ -3611,4 +3649,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/prowler/config/config.py b/prowler/config/config.py
index 5bec954def..e2732a90f1 100644
--- a/prowler/config/config.py
+++ b/prowler/config/config.py
@@ -49,7 +49,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
-prowler_version = "5.34.0"
+prowler_version = "5.39.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"
@@ -81,6 +81,7 @@ class Provider(str, Enum):
OKTA = "okta"
STACKIT = "stackit"
LINODE = "linode"
+ HUAWEICLOUD = "huaweicloud"
E2ENETWORKS = "e2enetworks"
diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml
index 6d2803be56..7dd7c2bf3d 100644
--- a/prowler/config/config.yaml
+++ b/prowler/config/config.yaml
@@ -27,6 +27,8 @@ aws:
max_lambda_functions: null
# aws.max_ecs_task_definitions --> ecs_task_definitions_* checks
max_ecs_task_definitions: null
+ # aws.max_batch_job_definitions --> batch_job_definition_* checks
+ max_batch_job_definitions: null
# aws.max_codeartifact_packages --> codeartifact_packages_* checks
max_codeartifact_packages: null
# aws.disallowed_regions --> List of AWS regions to exclude from the scan.
@@ -63,6 +65,8 @@ aws:
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
+ # aws.ec2_instance_stopped_older_than_specific_days --> by default is 30 days
+ max_ec2_instance_stopped_days: 30
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
@@ -424,6 +428,21 @@ aws:
- "SecurityPolicy_TLS13_1_2_PFS_PQ_2025_09"
- "SecurityPolicy_TLS13_1_2_PQ_2025_09"
+ # aws.elbv2_listener_pqc_tls_enabled
+ # Allowed post-quantum TLS security policies for ELBv2 HTTPS/TLS listeners
+ elbv2_listener_pqc_tls_allowed_policies:
+ - "ELBSecurityPolicy-TLS13-1-2-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Ext1-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Ext2-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-3-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-FIPS-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Ext0-FIPS-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Ext1-FIPS-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Ext2-FIPS-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-PQ-2025-09"
+ - "ELBSecurityPolicy-TLS13-1-3-FIPS-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:
@@ -450,9 +469,11 @@ aws:
secrets_ignore_patterns: []
# aws.awslambda_function_no_secrets_in_code
- # Glob patterns of file names inside the Lambda deployment package to skip
- # when scanning for secrets. Useful to suppress known false positives such
- # as .NET dependency manifests.
+ # aws.codecommit_repository_no_secrets
+ # Glob patterns of file names inside the Lambda deployment package or the
+ # CodeCommit repository to skip when scanning for secrets. Useful to
+ # suppress known false positives such as .NET dependency manifests or
+ # package lock files.
# Example:
# secrets_ignore_files:
# - "*.deps.json"
diff --git a/prowler/config/scan_config_schema.py b/prowler/config/scan_config_schema.py
index ac00250c78..fa445f33e9 100644
--- a/prowler/config/scan_config_schema.py
+++ b/prowler/config/scan_config_schema.py
@@ -9,21 +9,49 @@ 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`.
+- `validate_and_normalize_scan_config(payload)` — STRICT: returns
+ ``(normalized, errors)``. When ``errors`` is non-empty the normalized
+ dictionary is empty so callers never persist a partially validated
+ configuration. On success the normalized payload is JSON-serializable
+ (`model_dump(mode="json", exclude_unset=True)`), so the API can store
+ it directly in a Django ``JSONField`` and consume it at scan time
+ without re-running schema validation.
+
+- `validate_scan_config(payload)` — thin backward-compatible wrapper that
+ returns only the validation errors, preserved for callers that don't
+ need the normalized payload.
- `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.
"""
+import json
+from functools import lru_cache
from typing import Any
from pydantic import ValidationError
from prowler.config.schema.registry import SCHEMAS
+from prowler.lib.check.check import list_services
+from prowler.lib.check.models import CheckMetadata
+
+# Pydantic v2 prefixes messages emitted from a ``field_validator`` that
+# raises ``ValueError`` with this string. Strip it so the message that
+# reaches the UI is the one the validator actually wrote.
+_PYDANTIC_VALUE_ERROR_PREFIX = "Value error, "
+
+
+@lru_cache(maxsize=None)
+def _get_provider_check_ids(provider: str) -> frozenset[str]:
+ """Return cached check identifiers for a provider."""
+ return frozenset(CheckMetadata.get_bulk(provider))
+
+
+@lru_cache(maxsize=None)
+def _get_provider_services(provider: str) -> frozenset[str]:
+ """Return cached service identifiers for a provider."""
+ return frozenset(list_services(provider))
def _format_loc(loc: tuple) -> str:
@@ -50,48 +78,145 @@ def _format_loc(loc: tuple) -> str:
return ".".join(parts) if parts else ""
-def validate_scan_config(payload: Any) -> list[dict]:
- """Validate a scan config payload against the registered provider schemas.
+def validate_and_normalize_scan_config(
+ payload: Any,
+) -> tuple[dict, list[dict[str, str]]]:
+ """Strict validation and normalization of a scan configuration payload.
- 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).
+ Returns ``(normalized, errors)``:
+
+ - ``normalized`` is a JSON-serializable dict that mirrors the layout of
+ ``prowler/config/config.yaml`` (keyed by provider type). Registered
+ provider sections are dumped from their Pydantic models with
+ ``mode="json"`` (so the API can persist the result in a Django
+ ``JSONField``) and ``exclude_unset=True`` (so omitted defaults are
+ not injected into pre-existing configurations). Unknown provider
+ sections and unknown keys inside registered sections are preserved
+ untouched for forward compatibility with plugin-provided keys.
+ - ``errors`` is a list of ``{"path": , "message": }``
+ entries, one per schema or exclusion-catalog violation. When any error
+ is present the normalized dictionary is returned empty so the caller
+ never persists a partially validated configuration.
+
+ The input payload is never mutated.
"""
if not isinstance(payload, dict):
- return [
+ return {}, [
{
"path": "",
"message": "Scan config must be a mapping with provider sections.",
}
]
- errors: list[dict] = []
+ errors: list[dict[str, str]] = []
+ normalized: dict[str, Any] = {}
+
for provider, section in payload.items():
- schema_cls = SCHEMAS.get(provider)
+ # Reject non-string provider keys so distinct entries like ``123``
+ # and ``"123"`` don't collide after ``str()`` in the normalized dict.
+ # YAML always produces string keys at this level; anything else
+ # comes from a hand-built payload and is a caller bug.
+ if not isinstance(provider, str):
+ errors.append(
+ {
+ "path": repr(provider),
+ "message": "provider keys must be strings.",
+ }
+ )
+ continue
+
+ provider_key = provider
+ schema_cls = SCHEMAS.get(provider_key)
if schema_cls is None:
- # Unknown provider type: tolerated. The SDK will simply ignore it.
+ # Unknown provider type: tolerated, but only when its contents
+ # are already JSON-serializable. The API persists the returned
+ # payload in a Django ``JSONField`` and would blow up at write
+ # time if we let a ``set()`` or similar through here.
+ try:
+ json.dumps(section)
+ except (TypeError, ValueError) as exc:
+ errors.append(
+ {
+ "path": provider_key,
+ "message": (
+ "unknown provider section is not JSON-serializable: "
+ f"{exc}"
+ ),
+ }
+ )
+ continue
+ normalized[provider_key] = section
continue
if not isinstance(section, dict):
errors.append(
{
- "path": str(provider),
+ "path": provider_key,
"message": "section must be a mapping.",
}
)
continue
try:
- schema_cls.model_validate(section)
+ model = 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"),
- }
- )
+ path = _format_loc((provider_key, *loc))
+ message = err.get("msg", "validation error")
+ # Only strip on the specific error type that pydantic
+ # prefixes — a legitimate future message that happens to
+ # start with "Value error, " keeps its text intact.
+ if err.get("type") == "value_error" and message.startswith(
+ _PYDANTIC_VALUE_ERROR_PREFIX
+ ):
+ message = message[len(_PYDANTIC_VALUE_ERROR_PREFIX) :]
+ errors.append({"path": path, "message": message})
+ continue
+
+ if model.excluded_checks:
+ available_checks = _get_provider_check_ids(provider_key)
+ for index, check in enumerate(model.excluded_checks):
+ if check not in available_checks:
+ errors.append(
+ {
+ "path": f"{provider_key}.excluded_checks[{index}]",
+ "message": (
+ f"Unknown check '{check}' for provider "
+ f"'{provider_key}'."
+ ),
+ }
+ )
+
+ if model.excluded_services:
+ available_services = _get_provider_services(provider_key)
+ for index, service in enumerate(model.excluded_services):
+ if service not in available_services:
+ errors.append(
+ {
+ "path": f"{provider_key}.excluded_services[{index}]",
+ "message": (
+ f"Unknown service '{service}' for provider "
+ f"'{provider_key}'."
+ ),
+ }
+ )
+
+ normalized[provider_key] = model.model_dump(mode="json", exclude_unset=True)
+
+ if errors:
+ return {}, errors
+ return normalized, []
+
+
+def validate_scan_config(payload: Any) -> list[dict]:
+ """Backward-compatible wrapper returning only validation errors.
+
+ Preserved for callers that only need the strict-validation error list
+ (e.g. the DRF serializer that turns each entry into a
+ ``ValidationError``). New callers should prefer
+ :func:`validate_and_normalize_scan_config` to also receive the
+ normalized payload.
+ """
+ _, errors = validate_and_normalize_scan_config(payload)
return errors
@@ -100,9 +225,20 @@ def _build_aggregated_schema() -> dict:
The output mirrors the layout of `prowler/config/config.yaml` (a mapping
keyed by provider type) and is what the UI consumes via `ajv`.
+
+ Only app-facing providers (`sdk_only = False`, see
+ `Provider.get_app_providers`) are included. SDK/CLI-only providers may
+ still have a schema registered in `SCHEMAS` so the CLI validates their
+ `config.yaml` (`load_and_validate_config_file` reads `SCHEMAS.get`), but
+ they must not surface in this app-facing schema.
"""
+ from prowler.providers.common.provider import Provider
+
+ app_providers = set(Provider.get_app_providers())
properties: dict[str, dict] = {}
for provider, schema_cls in SCHEMAS.items():
+ if provider not in app_providers:
+ continue
properties[provider] = schema_cls.model_json_schema()
return {
"$schema": "https://json-schema.org/draft/2020-12/schema",
diff --git a/prowler/config/schema/aws.py b/prowler/config/schema/aws.py
index 4e52093029..c0ecf9a21c 100644
--- a/prowler/config/schema/aws.py
+++ b/prowler/config/schema/aws.py
@@ -153,6 +153,12 @@ class AWSProviderConfig(ProviderConfigBase):
le=1_000_000,
description="Resource scan limit for ECS task definitions. Use 0 or -1 to disable.",
)
+ max_batch_job_definitions: ResourceScanLimit = Field(
+ default=None,
+ ge=-1,
+ le=1_000_000,
+ description="Resource scan limit for Batch job definitions. Use 0 or -1 to disable.",
+ )
max_codeartifact_packages: ResourceScanLimit = Field(
default=None,
ge=-1,
@@ -213,6 +219,15 @@ class AWSProviderConfig(ProviderConfigBase):
"per NIST CM-3 — anything older is a security smell)."
),
)
+ max_ec2_instance_stopped_days: Optional[int] = Field(
+ default=None,
+ ge=1,
+ le=1095,
+ description=(
+ "Days an EC2 instance can remain stopped before being flagged. "
+ "Range: 1..1095 (3 years)."
+ ),
+ )
ec2_allowed_interface_types: Optional[list[str]] = None
ec2_allowed_instance_owners: Optional[list[str]] = None
ec2_high_risk_ports: Annotated[
@@ -415,6 +430,10 @@ class AWSProviderConfig(ProviderConfigBase):
le=6,
description="Min AZs an Application/Network LB must span. Range: 1..6.",
)
+ elbv2_listener_pqc_tls_allowed_policies: Optional[list[str]] = Field(
+ default=None,
+ description="ELBv2 SSL policies that satisfy the PQ TLS listener check.",
+ )
# --- ElastiCache -----------------------------------------------------
minimum_snapshot_retention_period: Optional[int] = Field(
diff --git a/prowler/config/schema/base.py b/prowler/config/schema/base.py
index cc473a4545..fc5a76af43 100644
--- a/prowler/config/schema/base.py
+++ b/prowler/config/schema/base.py
@@ -1,4 +1,12 @@
-from pydantic import BaseModel, ConfigDict
+from typing import Annotated
+
+from pydantic import BaseModel, ConfigDict, Field, StringConstraints, field_validator
+
+# Item type for excluded_checks / excluded_services list entries. Item
+# whitespace is stripped via ``str_strip_whitespace`` on the base
+# ``model_config`` (no second stripping implementation added here), so
+# ``min_length=1`` catches "", " ", and any all-whitespace input uniformly.
+NonEmptyScopeIdentifier = Annotated[str, StringConstraints(min_length=1)]
class ProviderConfigBase(BaseModel):
@@ -15,3 +23,28 @@ class ProviderConfigBase(BaseModel):
str_strip_whitespace=True,
validate_assignment=False,
)
+
+ excluded_checks: list[NonEmptyScopeIdentifier] = Field(
+ default_factory=list,
+ description="Check identifiers to exclude from the scan scope.",
+ json_schema_extra={"default": [], "uniqueItems": True},
+ )
+ excluded_services: list[NonEmptyScopeIdentifier] = Field(
+ default_factory=list,
+ description="Service identifiers to exclude from the scan scope.",
+ json_schema_extra={"default": [], "uniqueItems": True},
+ )
+
+ @field_validator("excluded_checks", "excluded_services")
+ @classmethod
+ def _reject_duplicates(cls, value: list[str]) -> list[str]:
+ seen: set[str] = set()
+ duplicates: set[str] = set()
+ for item in value:
+ if item in seen:
+ duplicates.add(item)
+ else:
+ seen.add(item)
+ if duplicates:
+ raise ValueError(f"duplicate values are not allowed: {sorted(duplicates)}")
+ return value
diff --git a/prowler/lib/banner.py b/prowler/lib/banner.py
index 8115983bc6..e1c7fa5a35 100644
--- a/prowler/lib/banner.py
+++ b/prowler/lib/banner.py
@@ -2,6 +2,21 @@ from colorama import Fore, Style
from prowler.config.config import banner_color, orange_color, prowler_version, timestamp
+# Prowler Cloud landing URL used by the CLI banner. The visible text stays
+# "cloud.prowler.com" while the clickable target carries the UTM source so
+# terminals that support OSC 8 hyperlinks attribute the visit to the CLI.
+CLOUD_DISPLAY_TEXT = "cloud.prowler.com"
+CLOUD_BANNER_URL = "https://cloud.prowler.com/sign-up?utm_source=prowler-cli"
+
+
+def _hyperlink(url: str, text: str) -> str:
+ """Wrap ``text`` in an OSC 8 terminal hyperlink pointing to ``url``.
+
+ Terminals that support OSC 8 render ``text`` as a clickable link to ``url``;
+ those that do not simply display ``text`` unchanged.
+ """
+ return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
+
def print_banner(legend: bool = False, provider: str = None):
"""
@@ -18,8 +33,8 @@ def print_banner(legend: bool = False, provider: str = None):
_ __ _ __ _____ _| | ___ _ __
| '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|
| |_) | | | (_) \ V V /| | __/ |
-| .__/|_| \___/ \_/\_/ |_|\___|_|v{prowler_version}
-|_|{Fore.BLUE} Get the most at https://cloud.prowler.com {Style.RESET_ALL}
+| .__/|_| \___/ \_/\_/ |_|\___|_| CLI - v{prowler_version}
+|_|
{Fore.YELLOW}Date: {timestamp.strftime("%Y-%m-%d %H:%M:%S")}{Style.RESET_ALL}
"""
@@ -43,8 +58,9 @@ def print_prowler_cloud_banner(provider: str = None):
the open-source CLI.
Shown at the start and end of a scan to let users know about the managed
- platform capabilities they are missing (attack paths, AI, organizations,
- continuous scanning, integrations and live compliance dashboards).
+ platform capabilities they are missing (CLI findings upload, attack paths,
+ AI, triage, organizations, continuous scanning with custom scheduling and
+ scan configuration, integrations and live compliance dashboards).
Parameters:
- provider (str): The provider that was scanned, used to tailor the message.
@@ -57,7 +73,9 @@ def print_prowler_cloud_banner(provider: str = None):
print(f"""
{bar} {Style.BRIGHT}You're getting a snapshot 📸. Prowler Cloud gives you the full picture:{Style.RESET_ALL}
{bar}
-{bar} {check} {Style.BRIGHT}Continuous Security Monitoring{Style.RESET_ALL} - scheduled scans with history, trends and alerts.
+{bar} {check} {Style.BRIGHT}Send your findings{Style.RESET_ALL} - directly from the Prowler CLI to Prowler Cloud.
+{bar} {check} {Style.BRIGHT}Continuous Security Monitoring{Style.RESET_ALL} - custom scheduling and scan configuration with history, trends and alerts.
+{bar} {check} {Style.BRIGHT}Triage{Style.RESET_ALL} - review findings, flag false positives and track accepted risk with your team.
{bar} {check} {Style.BRIGHT}Lighthouse AI + MCP{Style.RESET_ALL} - autonomous triage, custom dashboards, prioritization with prevention and remediation.
{bar} {check} {Style.BRIGHT}Alerts{Style.RESET_ALL} - get notified when anything you want is happening.
{bar} {check} {Style.BRIGHT}Live Compliance{Style.RESET_ALL} - dashboards for 50+ frameworks, always up to date.
@@ -66,5 +84,5 @@ def print_prowler_cloud_banner(provider: str = None):
{bar} {check} {Style.BRIGHT}Bulk Provisioning{Style.RESET_ALL} - add your entire AWS Organization in seconds.
{bar} {check} {Style.BRIGHT}Integrations{Style.RESET_ALL} - Anything with our MCP + Jira, Slack, AWS Security Hub, Amazon S3, SSO and RBAC.
{bar}
-{bar} {Fore.BLUE}Start free at 👉 cloud.prowler.com{Style.RESET_ALL}
+{bar} {banner_color}Start free at 👉 {_hyperlink(CLOUD_BANNER_URL, CLOUD_DISPLAY_TEXT)}{Style.RESET_ALL}
""")
diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py
index c0c6a02e5d..283d8d2e8b 100644
--- a/prowler/lib/check/check.py
+++ b/prowler/lib/check/check.py
@@ -21,7 +21,11 @@ from prowler.lib.check.utils import recover_checks_from_provider
from prowler.lib.logger import logger
from prowler.lib.outputs.outputs import report
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
-from prowler.providers.common.builtin import is_builtin_provider
+from prowler.providers.common.builtin import (
+ builtin_check_module,
+ is_builtin_check,
+ is_builtin_provider,
+)
from prowler.providers.common.models import Audit_Metadata
@@ -401,21 +405,23 @@ def _resolve_check_module(
when a plug-in tries to override, so the user knows their plug-in
duplicate is being ignored and can rename it.
- Gates the built-in branch on `is_builtin_provider(provider_type)` —
- calling `find_spec` on `prowler.providers.{provider_type}.services...`
- directly would propagate `ModuleNotFoundError` for external providers
- (their parent package `prowler.providers.{provider_type}` does not
- exist) instead of returning None. The leaf helper encapsulates the
- safe lookup, so external providers go straight to entry points. For
- built-ins we still use `find_spec` to distinguish "check doesn't
- exist" from "check exists but failed to import" (broken transitive
- dep, etc.).
+ Both probes are gated on leaf helpers rather than a raw `find_spec`,
+ because `find_spec` imports the parent package in order to search it and
+ so propagates `ModuleNotFoundError` instead of returning None whenever
+ that parent is absent. That happens on both axes: for an external
+ provider (no `prowler.providers.{provider_type}` package) and, on a
+ built-in provider, for an external check (no
+ `prowler.providers.{provider_type}.services.{service}.{check_name}`
+ package). Either one, probed naively, aborts the lookup before the entry
+ points are ever consulted. `is_builtin_check` still distinguishes "check
+ doesn't exist" from "check exists but failed to import" (broken
+ transitive dep, etc.), which a blanket except would flatten.
"""
# Built-in first — built-in wins on CheckID collision
- if is_builtin_provider(provider_type):
- builtin_path = f"prowler.providers.{provider_type}.services.{service}.{check_name}.{check_name}"
- if importlib.util.find_spec(builtin_path) is not None:
- return import_check(builtin_path)
+ if is_builtin_provider(provider_type) and is_builtin_check(
+ provider_type, service, check_name
+ ):
+ return import_check(builtin_check_module(provider_type, service, check_name))
# Entry point lookup — only consulted when the built-in truly doesn't exist
for ep in importlib.metadata.entry_points(group=f"prowler.checks.{provider_type}"):
@@ -801,6 +807,10 @@ def execute(
is_finding_muted_args["account_id"] = (
global_provider.identity.account_id
)
+ elif global_provider.type == "huaweicloud":
+ 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()
diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py
index 5f92ecf481..bd346ad8b3 100644
--- a/prowler/lib/check/models.py
+++ b/prowler/lib/check/models.py
@@ -903,6 +903,31 @@ class CheckReportAlibabaCloud(Check_Report):
self.region = getattr(resource, "region", "")
+@dataclass
+class CheckReportHuaweiCloud(Check_Report):
+ """Contains the Huawei Cloud Check's finding information."""
+
+ resource_id: str
+ resource_arn: str
+ region: str
+ resource_name: str
+
+ def __init__(self, metadata: Dict, resource: Any) -> None:
+ """Initialize the Huawei Cloud Check's finding information.
+
+ Args:
+ metadata: The metadata of the check.
+ resource: Basic information about the resource.
+ """
+ super().__init__(metadata, resource)
+ self.resource_id = (
+ getattr(resource, "id", None) or getattr(resource, "name", None) or ""
+ )
+ self.resource_arn = getattr(resource, "arn", "")
+ self.region = getattr(resource, "region", "")
+ self.resource_name = getattr(resource, "name", "") or self.resource_id
+
+
@dataclass
class Check_Report_Kubernetes(Check_Report):
# TODO change class name to CheckReportKubernetes
diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py
index 38661e6445..68c08e8f6b 100644
--- a/prowler/lib/cli/parser.py
+++ b/prowler/lib/cli/parser.py
@@ -53,6 +53,7 @@ class ProwlerArgumentParser:
"scaleway",
"stackit",
"linode",
+ "huaweicloud",
}
all_providers = set(Provider.get_available_providers())
new_providers = sorted(all_providers - known_providers)
@@ -75,10 +76,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,linode,e2enetworks,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,huaweicloud,e2enetworks,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,linode,e2enetworks{extra_providers_csv}}}
+ {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,huaweicloud,e2enetworks{extra_providers_csv}}}
aws AWS Provider
azure Azure Provider
gcp GCP Provider
@@ -100,6 +101,7 @@ Available Cloud Providers:
scaleway Scaleway Provider
vercel Vercel Provider
linode Linode Provider
+ huaweicloud Huawei Cloud Provider
e2enetworks E2E Networks Provider{extra_providers_text}
diff --git a/prowler/lib/outputs/compliance/universal/universal_output.py b/prowler/lib/outputs/compliance/universal/universal_output.py
index b1b0d9409b..5cca376482 100644
--- a/prowler/lib/outputs/compliance/universal/universal_output.py
+++ b/prowler/lib/outputs/compliance/universal/universal_output.py
@@ -26,6 +26,7 @@ PROVIDER_HEADER_MAP = {
"oraclecloud": ("TenancyId", "account_uid", "Region", "region"),
"alibabacloud": ("AccountId", "account_uid", "Region", "region"),
"nhn": ("AccountId", "account_uid", "Region", "region"),
+ "huaweicloud": ("AccountId", "account_uid", "Region", "region"),
"e2enetworks": ("ProjectId", "account_uid", "Location", "region"),
}
_DEFAULT_HEADERS = ("AccountId", "account_uid", "Region", "region")
diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py
index 7231572571..dc88044692 100644
--- a/prowler/lib/outputs/finding.py
+++ b/prowler/lib/outputs/finding.py
@@ -514,6 +514,23 @@ class Finding(BaseModel):
)
output_data["region"] = check_output.region
+ elif provider.type == "huaweicloud":
+ output_data["auth_method"] = get_nested_attribute(
+ provider, "identity.identity_type"
+ )
+ output_data["account_uid"] = get_nested_attribute(
+ provider, "identity.account_id"
+ )
+ output_data["account_name"] = get_nested_attribute(
+ provider, "identity.account_name"
+ )
+ output_data["resource_name"] = check_output.resource_name
+ output_data["resource_uid"] = (
+ getattr(check_output, "resource_arn", "")
+ or check_output.resource_id
+ )
+ output_data["region"] = check_output.region
+
elif provider.type == "openstack":
output_data["auth_method"] = (
f"Username: {get_nested_attribute(provider, 'identity.username')}"
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index 3463014232..1dcbfb5416 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -1,5 +1,7 @@
+import re
import sys
from io import TextIOWrapper
+from urllib.parse import urlparse
import markdown
from markupsafe import escape
@@ -16,6 +18,33 @@ from prowler.lib.outputs.output import Finding, Output
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
from prowler.providers.common.provider import Provider
+_SAFE_URL_SCHEMES = {"http", "https"}
+
+
+def _safe_url(url: str) -> str:
+ """Return url if its scheme is http/https, otherwise return empty string."""
+ if not url:
+ return ""
+ scheme = urlparse(url).scheme.lower()
+ return url if scheme in _SAFE_URL_SCHEMES else ""
+
+
+def _strip_unsafe_links(html_content: str) -> str:
+ """Replace tags whose href is not http/https with their link text."""
+
+ def _replace(match: re.Match) -> str:
+ href = match.group("href")
+ body = match.group("body")
+ safe = _safe_url(href)
+ return f'{body}' if safe else body
+
+ return re.sub(
+ r']*href="(?P[^"]*)"[^>]*>(?P.*?)',
+ _replace,
+ html_content,
+ flags=re.IGNORECASE | re.DOTALL,
+ )
+
class HTML(Output):
@staticmethod
@@ -52,7 +81,7 @@ class HTML(Output):
html_content = html_content.replace("
", "")
html_content = html_content.replace("
", "")
- return html_content
+ return _strip_unsafe_links(html_content)
def transform(self, findings: list[Finding]) -> None:
"""Transforms the findings into the HTML format.
@@ -77,16 +106,16 @@ class HTML(Output):
self._data.append(f"""