mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
Merge remote-tracking branch 'origin/master' into feature/eslint-typescript-flat
# Conflicts: # ui/pnpm-lock.yaml
This commit is contained in:
@@ -158,7 +158,7 @@ SENTRY_RELEASE=local
|
||||
# REO_DEV_CLIENT_ID=
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.35.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.36.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -4,6 +4,20 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.36.0] (Prowler v5.35.0)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `attack-paths-scan-perform` Celery tasks now use the configurable long-task time limits instead of the six-hour defaults [(#12009)](https://github.com/prowler-cloud/prowler/pull/12009)
|
||||
- Attack Paths scans handle provider deletion races cleanly, detect stale tasks after 16 hours, use backend-specific graph synchronization batches, and report exhausted Neptune write retries with the original database error [(#12019)](https://github.com/prowler-cloud/prowler/pull/12019)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Jira integration credentials only accept bare Atlassian site names containing letters, numbers, and hyphens [(#12012)](https://github.com/prowler-cloud/prowler/pull/12012)
|
||||
- Social account linking requires a verified matching email from both the identity provider and the existing user account without sending account connection notifications [(#12013)](https://github.com/prowler-cloud/prowler/pull/12013)
|
||||
|
||||
---
|
||||
|
||||
## [1.35.0] (Prowler v5.34.0)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Attack Paths scans handle provider deletion races cleanly, detect stale tasks after 16 hours, use backend-specific graph synchronization batches, and report exhausted Neptune write retries with the original database error
|
||||
@@ -1 +0,0 @@
|
||||
`attack-paths-scan-perform` Celery tasks now use the configurable long-task time limits instead of the six-hour defaults
|
||||
@@ -1 +0,0 @@
|
||||
Jira integration credentials only accept bare Atlassian site names containing letters, numbers, and hyphens
|
||||
@@ -0,0 +1 @@
|
||||
OCI provider secrets no longer require `region`; legacy `region` input is accepted for backwards compatibility but ignored before storing or scanning
|
||||
@@ -1 +0,0 @@
|
||||
Social account linking requires a verified matching email from both the identity provider and the existing user account without sending account connection notifications
|
||||
+1
-1
@@ -71,7 +71,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.36.0"
|
||||
version = "1.37.0"
|
||||
|
||||
# Shared ruff baseline (kept in sync with mcp_server/pyproject.toml).
|
||||
# target-version tracks this project's lowest supported Python.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.36.0
|
||||
version: 1.37.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ from api.v1.serializer_utils.integrations import (
|
||||
JiraCredentialSerializer,
|
||||
S3ConfigSerializer,
|
||||
)
|
||||
from api.v1.serializers import ImageProviderSecret, KubernetesProviderSecret
|
||||
from api.v1.serializer_utils.providers import ProviderSecretField
|
||||
from api.v1.serializers import (
|
||||
ImageProviderSecret,
|
||||
KubernetesProviderSecret,
|
||||
OracleCloudProviderSecret,
|
||||
)
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
@@ -190,6 +195,64 @@ class TestImageProviderSecret:
|
||||
assert "non_field_errors" in serializer.errors
|
||||
|
||||
|
||||
class TestOracleCloudProviderSecret:
|
||||
def valid_secret(self, **overrides):
|
||||
secret = {
|
||||
"user": "ocid1.user.oc1..aaaaaaaexample",
|
||||
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
|
||||
"key_content": "fake-base64-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
}
|
||||
secret.update(overrides)
|
||||
return secret
|
||||
|
||||
def test_accepts_regionless_secret(self):
|
||||
serializer = OracleCloudProviderSecret(data=self.valid_secret())
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert "region" not in serializer.validated_data
|
||||
|
||||
def test_accepts_and_ignores_region_field(self):
|
||||
secret = self.valid_secret(region="us-phoenix-1")
|
||||
serializer = OracleCloudProviderSecret(data=secret)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
assert "region" not in serializer.validated_data
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"legacy_field, legacy_value",
|
||||
[
|
||||
("region", None),
|
||||
("region", ""),
|
||||
("region", {"name": "us-ashburn-1"}),
|
||||
],
|
||||
)
|
||||
def test_accepts_and_ignores_any_legacy_region_value(
|
||||
self, legacy_field, legacy_value
|
||||
):
|
||||
serializer = OracleCloudProviderSecret(
|
||||
data=self.valid_secret(**{legacy_field: legacy_value})
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
assert legacy_field not in serializer.validated_data
|
||||
|
||||
|
||||
class TestProviderSecretFieldSchema:
|
||||
def test_oraclecloud_schema_includes_legacy_region_field(self):
|
||||
schema = ProviderSecretField._spectacular_annotation["field"]
|
||||
oraclecloud_schema = next(
|
||||
credential_schema
|
||||
for credential_schema in schema["oneOf"]
|
||||
if credential_schema["title"]
|
||||
== "Oracle Cloud Infrastructure (OCI) API Key Credentials"
|
||||
)
|
||||
|
||||
assert oraclecloud_schema["properties"]["region"]["deprecated"] is True
|
||||
|
||||
|
||||
class TestKubernetesProviderSecret:
|
||||
def test_valid_static_kubeconfig_is_accepted(self):
|
||||
kubeconfig_content = """
|
||||
|
||||
@@ -171,6 +171,53 @@ class TestInitializeProwlerProvider:
|
||||
key="value", mutelist_content={"key": "value"}
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_initialize_oraclecloud_provider_removes_region_string(
|
||||
self, mock_return_prowler_provider
|
||||
):
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.ORACLECLOUD.value
|
||||
provider.secret.secret = {
|
||||
"user": "ocid1.user.oc1..fake",
|
||||
"fingerprint": "00:11:22:33:44:55:66:77",
|
||||
"key_content": "fake-base64-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..fake",
|
||||
"region": "us-ashburn-1",
|
||||
}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
initialize_prowler_provider(provider)
|
||||
|
||||
mock_return_prowler_provider.return_value.assert_called_once_with(
|
||||
user="ocid1.user.oc1..fake",
|
||||
fingerprint="00:11:22:33:44:55:66:77",
|
||||
key_content="fake-base64-key-content",
|
||||
tenancy="ocid1.tenancy.oc1..fake",
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_initialize_oraclecloud_provider_without_region_omits_scan_filter(
|
||||
self, mock_return_prowler_provider
|
||||
):
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.ORACLECLOUD.value
|
||||
provider.secret.secret = {
|
||||
"user": "ocid1.user.oc1..fake",
|
||||
"fingerprint": "00:11:22:33:44:55:66:77",
|
||||
"key_content": "fake-base64-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..fake",
|
||||
}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
initialize_prowler_provider(provider)
|
||||
|
||||
mock_return_prowler_provider.return_value.assert_called_once_with(
|
||||
user="ocid1.user.oc1..fake",
|
||||
fingerprint="00:11:22:33:44:55:66:77",
|
||||
key_content="fake-base64-key-content",
|
||||
tenancy="ocid1.tenancy.oc1..fake",
|
||||
)
|
||||
|
||||
|
||||
class TestProwlerProviderConnectionTest:
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
@@ -185,6 +232,37 @@ class TestProwlerProviderConnectionTest:
|
||||
key="value", provider_id="1234567890", raise_on_exception=False
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_oraclecloud_connection_test_uses_direct_credentials_without_region(
|
||||
self, mock_return_prowler_provider
|
||||
):
|
||||
provider = MagicMock()
|
||||
provider.uid = "ocid1.tenancy.oc1..aaaaaaaexample"
|
||||
provider.provider = Provider.ProviderChoices.ORACLECLOUD.value
|
||||
provider.secret.secret = {
|
||||
"user": "ocid1.user.oc1..aaaaaaaexample",
|
||||
"fingerprint": "00:11:22:33:44:55:66:77",
|
||||
"key_content": "fake-base64-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
prowler_provider_connection_test(provider)
|
||||
|
||||
mock_return_prowler_provider.return_value.test_connection.assert_called_once_with(
|
||||
user="ocid1.user.oc1..aaaaaaaexample",
|
||||
fingerprint="00:11:22:33:44:55:66:77",
|
||||
key_content="fake-base64-key-content",
|
||||
tenancy="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
region=getattr(
|
||||
OraclecloudProvider,
|
||||
"_bootstrap_region",
|
||||
OraclecloudProvider._home_region,
|
||||
),
|
||||
provider_id="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_prowler_provider_connection_test_without_secret(
|
||||
@@ -356,7 +434,7 @@ class TestGetProwlerProviderKwargs:
|
||||
expected_result = {**secret_dict, **expected_extra_kwargs}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_oraclecloud_converts_region_string_to_set(
|
||||
def test_get_prowler_provider_kwargs_oraclecloud_removes_region(
|
||||
self,
|
||||
):
|
||||
secret_dict = {
|
||||
@@ -377,8 +455,13 @@ class TestGetProwlerProviderKwargs:
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = {**secret_dict, "region": {"us-ashburn-1"}}
|
||||
assert result == expected_result
|
||||
assert result == {
|
||||
"user": "ocid1.user.oc1..fake",
|
||||
"fingerprint": "00:11:22:33:44:55:66:77",
|
||||
"key_content": "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----",
|
||||
"tenancy": "ocid1.tenancy.oc1..fake",
|
||||
"pass_phrase": "fake-passphrase",
|
||||
}
|
||||
|
||||
def test_get_prowler_provider_kwargs_with_mutelist(self):
|
||||
provider_uid = "provider_uid"
|
||||
|
||||
@@ -2917,6 +2917,48 @@ class TestProviderGroupViewSet:
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderSecretViewSet:
|
||||
@staticmethod
|
||||
def _oraclecloud_secret(**overrides):
|
||||
secret = {
|
||||
"user": "ocid1.user.oc1..aaaaaaaakldibrbov4ubh25aqdeiroklxjngwka7u6w7no3glmdq3n5sxtkq",
|
||||
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
|
||||
"key_content": "test-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda",
|
||||
}
|
||||
secret.update(overrides)
|
||||
return secret
|
||||
|
||||
def _create_oraclecloud_secret(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
secret,
|
||||
name="OCI Secret",
|
||||
):
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-secrets",
|
||||
"attributes": {
|
||||
"name": name,
|
||||
"secret_type": ProviderSecret.TypeChoices.STATIC,
|
||||
"secret": secret,
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"id": str(oraclecloud_provider.id),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return authenticated_client.post(
|
||||
reverse("providersecret-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
def test_provider_secrets_list(self, authenticated_client, provider_secret_fixture):
|
||||
response = authenticated_client.get(reverse("providersecret-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -3076,7 +3118,6 @@ current-context: test-context
|
||||
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
|
||||
"key_content": "-----BEGIN RSA PRIVATE KEY-----\ntest-key-content\n-----END RSA PRIVATE KEY-----",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda",
|
||||
"region": "us-ashburn-1",
|
||||
},
|
||||
),
|
||||
# OCI with API key credentials (with key_file)
|
||||
@@ -3088,7 +3129,6 @@ current-context: test-context
|
||||
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
|
||||
"key_file": "/path/to/oci_api_key.pem",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda",
|
||||
"region": "us-ashburn-1",
|
||||
},
|
||||
),
|
||||
# OCI with API key credentials (with passphrase)
|
||||
@@ -3100,7 +3140,6 @@ current-context: test-context
|
||||
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
|
||||
"key_content": "-----BEGIN RSA PRIVATE KEY-----\ntest-encrypted-key\n-----END RSA PRIVATE KEY-----",
|
||||
"tenancy": "ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda",
|
||||
"region": "us-ashburn-1",
|
||||
"pass_phrase": "my-secure-passphrase",
|
||||
},
|
||||
),
|
||||
@@ -3258,6 +3297,103 @@ current-context: test-context
|
||||
== data["data"]["relationships"]["provider"]["data"]["id"]
|
||||
)
|
||||
|
||||
def test_provider_secrets_create_oraclecloud_without_region_stores_no_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
):
|
||||
response = self._create_oraclecloud_secret(
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
self._oraclecloud_secret(),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
provider_secret = ProviderSecret.objects.get()
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
def test_provider_secrets_create_oraclecloud_accepts_and_ignores_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
):
|
||||
response = self._create_oraclecloud_secret(
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
self._oraclecloud_secret(
|
||||
key_content=" test-key-content ", region=" us-ashburn-1 "
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
provider_secret = ProviderSecret.objects.get()
|
||||
assert provider_secret.secret["key_content"] == "test-key-content"
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
def test_provider_secrets_update_oraclecloud_without_region_stores_no_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
):
|
||||
create_response = self._create_oraclecloud_secret(
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
self._oraclecloud_secret(),
|
||||
)
|
||||
provider_secret = ProviderSecret.objects.get(
|
||||
id=create_response.json()["data"]["id"]
|
||||
)
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-secrets",
|
||||
"id": str(provider_secret.id),
|
||||
"attributes": {"secret": self._oraclecloud_secret()},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("providersecret-detail", kwargs={"pk": provider_secret.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
provider_secret.refresh_from_db()
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
def test_provider_secrets_update_oraclecloud_accepts_and_ignores_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
):
|
||||
create_response = self._create_oraclecloud_secret(
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
self._oraclecloud_secret(),
|
||||
)
|
||||
provider_secret = ProviderSecret.objects.get(
|
||||
id=create_response.json()["data"]["id"]
|
||||
)
|
||||
data = {
|
||||
"data": {
|
||||
"type": "provider-secrets",
|
||||
"id": str(provider_secret.id),
|
||||
"attributes": {
|
||||
"secret": self._oraclecloud_secret(region=" us-ashburn-1 ")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("providersecret-detail", kwargs={"pk": provider_secret.id}),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
provider_secret.refresh_from_db()
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attributes, error_code, error_pointer",
|
||||
(
|
||||
|
||||
@@ -252,12 +252,6 @@ def get_prowler_provider_kwargs(
|
||||
**prowler_provider_kwargs,
|
||||
"filter_accounts": [provider.uid],
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
|
||||
if isinstance(prowler_provider_kwargs.get("region"), str):
|
||||
prowler_provider_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"region": {prowler_provider_kwargs["region"]},
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.OPENSTACK.value:
|
||||
# clouds_yaml_content, clouds_yaml_cloud and provider_id are validated
|
||||
# in the provider itself, so it's not needed here.
|
||||
@@ -288,6 +282,11 @@ def get_prowler_provider_kwargs(
|
||||
**{k: v for k, v in prowler_provider_kwargs.items() if v},
|
||||
}
|
||||
|
||||
elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
|
||||
prowler_provider_kwargs = _normalize_oraclecloud_provider_kwargs(
|
||||
prowler_provider_kwargs
|
||||
)
|
||||
|
||||
if mutelist_processor:
|
||||
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
|
||||
# IaC and Image providers don't support mutelist (both use Trivy's built-in logic)
|
||||
@@ -300,6 +299,40 @@ def get_prowler_provider_kwargs(
|
||||
return prowler_provider_kwargs
|
||||
|
||||
|
||||
def _normalize_oraclecloud_provider_kwargs(secret: dict) -> dict:
|
||||
"""Normalize external OCI secret fields into SDK provider kwargs."""
|
||||
prowler_provider_kwargs = secret.copy()
|
||||
prowler_provider_kwargs.pop("region", None)
|
||||
|
||||
return prowler_provider_kwargs
|
||||
|
||||
|
||||
def _normalize_oraclecloud_connection_test_kwargs(secret: dict) -> dict:
|
||||
"""Normalize external OCI secret fields into test_connection kwargs."""
|
||||
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
|
||||
|
||||
prowler_provider_kwargs = secret.copy()
|
||||
prowler_provider_kwargs.pop("region", None)
|
||||
|
||||
if (
|
||||
prowler_provider_kwargs.get("user")
|
||||
and prowler_provider_kwargs.get("fingerprint")
|
||||
and prowler_provider_kwargs.get("tenancy")
|
||||
and (
|
||||
prowler_provider_kwargs.get("key_content")
|
||||
or prowler_provider_kwargs.get("key_file")
|
||||
)
|
||||
):
|
||||
# Connection validation needs one OCI endpoint, but scans remain unfiltered.
|
||||
prowler_provider_kwargs["region"] = getattr(
|
||||
OraclecloudProvider,
|
||||
"_bootstrap_region",
|
||||
OraclecloudProvider._home_region,
|
||||
)
|
||||
|
||||
return prowler_provider_kwargs
|
||||
|
||||
|
||||
def initialize_prowler_provider(
|
||||
provider: Provider,
|
||||
mutelist_processor: Processor | None = None,
|
||||
@@ -402,6 +435,15 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
|
||||
if prowler_provider_kwargs.get("registry_token"):
|
||||
image_kwargs["registry_token"] = prowler_provider_kwargs["registry_token"]
|
||||
return prowler_provider.test_connection(**image_kwargs)
|
||||
elif provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
|
||||
oraclecloud_kwargs = _normalize_oraclecloud_connection_test_kwargs(
|
||||
prowler_provider_kwargs
|
||||
)
|
||||
return prowler_provider.test_connection(
|
||||
**oraclecloud_kwargs,
|
||||
provider_id=provider.uid,
|
||||
raise_on_exception=False,
|
||||
)
|
||||
else:
|
||||
return prowler_provider.test_connection(
|
||||
**prowler_provider_kwargs,
|
||||
|
||||
@@ -295,16 +295,21 @@ from rest_framework_json_api import serializers
|
||||
"type": "string",
|
||||
"description": "The OCID of the tenancy.",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The OCI region identifier (e.g., us-ashburn-1, us-phoenix-1).",
|
||||
},
|
||||
"pass_phrase": {
|
||||
"type": "string",
|
||||
"description": "The passphrase for the private key, if encrypted.",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"deprecated": True,
|
||||
"description": "Legacy OCI region field accepted for backwards compatibility but ignored; OCI scans all regions.",
|
||||
},
|
||||
},
|
||||
"required": ["user", "fingerprint", "tenancy", "region"],
|
||||
"required": ["user", "fingerprint", "tenancy"],
|
||||
"anyOf": [
|
||||
{"required": ["key_file"]},
|
||||
{"required": ["key_content"]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
||||
@@ -1672,6 +1672,7 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
|
||||
validation_error.detail[f"secret/{key}"] = value
|
||||
del validation_error.detail[key]
|
||||
raise validation_error
|
||||
return serializer.validated_data
|
||||
|
||||
|
||||
class AwsProviderSecret(serializers.Serializer):
|
||||
@@ -1813,14 +1814,32 @@ class IacProviderSecret(serializers.Serializer):
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class LegacyOCIRegionField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class OracleCloudProviderSecret(serializers.Serializer):
|
||||
user = serializers.CharField()
|
||||
fingerprint = serializers.CharField()
|
||||
key_file = serializers.CharField(required=False)
|
||||
key_content = serializers.CharField(required=False)
|
||||
tenancy = serializers.CharField()
|
||||
region = serializers.CharField()
|
||||
pass_phrase = serializers.CharField(required=False)
|
||||
region = LegacyOCIRegionField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs.pop("region", None)
|
||||
|
||||
if "key_file" not in attrs and "key_content" not in attrs:
|
||||
raise serializers.ValidationError(
|
||||
{"key_file": "Either key_file or key_content must be provided."}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
resource_name = "provider-secrets"
|
||||
@@ -1965,7 +1984,11 @@ class ProviderSecretCreateSerializer(RLSSerializer, BaseWriteProviderSecretSeria
|
||||
secret = attrs.get("secret")
|
||||
|
||||
validated_attrs = super().validate(attrs)
|
||||
self.validate_secret_based_on_provider(provider.provider, secret_type, secret)
|
||||
validated_secret = self.validate_secret_based_on_provider(
|
||||
provider.provider, secret_type, secret
|
||||
)
|
||||
if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
|
||||
validated_attrs["secret"] = validated_secret
|
||||
return validated_attrs
|
||||
|
||||
|
||||
@@ -1997,7 +2020,11 @@ class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer):
|
||||
secret = attrs.get("secret")
|
||||
|
||||
validated_attrs = super().validate(attrs)
|
||||
self.validate_secret_based_on_provider(provider.provider, secret_type, secret)
|
||||
validated_secret = self.validate_secret_based_on_provider(
|
||||
provider.provider, secret_type, secret
|
||||
)
|
||||
if provider.provider == Provider.ProviderChoices.ORACLECLOUD.value:
|
||||
validated_attrs["secret"] = validated_secret
|
||||
return validated_attrs
|
||||
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -4673,8 +4673,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.32.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#5dac8a0a53272e4db68c476fb969dc03e88beb68" }
|
||||
version = "5.35.0"
|
||||
source = { git = "https://github.com/prowler-cloud/prowler.git?rev=master#f5ea116763aeffede9f399c8934fc280eaccd315" }
|
||||
dependencies = [
|
||||
{ name = "alibabacloud-actiontrail20200706" },
|
||||
{ name = "alibabacloud-credentials" },
|
||||
@@ -4762,7 +4762,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.36.0"
|
||||
version = "1.37.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
|
||||
@@ -128,8 +128,8 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.34.0"
|
||||
PROWLER_API_VERSION="5.34.0"
|
||||
PROWLER_UI_VERSION="5.35.0"
|
||||
PROWLER_API_VERSION="5.35.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -24,6 +24,9 @@ The Agentic Cloud Defender does more than answer questions, it helps teams **fin
|
||||
<Card title="Normal and Agentic Views" icon="table-columns">
|
||||
Switch between the standard interface and a chat-first agentic view.
|
||||
</Card>
|
||||
<Card title="Side Panel on Every Page" icon="comment-dots">
|
||||
Open Lighthouse AI as a side panel from any page to get help in context.
|
||||
</Card>
|
||||
<Card title="Provider Connection Checks" icon="plug-circle-check">
|
||||
Credentials are validated automatically when a provider is configured.
|
||||
</Card>
|
||||
@@ -37,6 +40,20 @@ Promoting the chat to a top-level view gives Lighthouse AI the room it needs for
|
||||
|
||||
<img src="/images/prowler-app/lighthouse/prowler-cloud/main-chat-page.png" alt="Lighthouse AI chat view 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.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse/prowler-cloud/side-panel-closed.png" alt="Collapsed Lighthouse AI side panel on a Prowler Cloud page, with the button to open it circled in red" />
|
||||
|
||||
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.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse/prowler-cloud/side-panel-open.png" alt="Lighthouse AI side panel open alongside a Prowler Cloud page" />
|
||||
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
@@ -63,6 +80,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?**
|
||||
@@ -71,7 +135,11 @@ 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?
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 432 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 662 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 878 KiB |
@@ -8,12 +8,14 @@ import { SubscriptionBanner } from "/snippets/subscription-banner.mdx"
|
||||
|
||||
<VersionBadge version="5.19.0" />
|
||||
|
||||
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.
|
||||
|
||||
<SubscriptionBanner>
|
||||
For CLI-based multi-account scanning, see [AWS Organizations in Prowler CLI](/user-guide/providers/aws/organizations).
|
||||
</SubscriptionBanner>
|
||||
|
||||
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
|
||||
|
||||
Onboarding deploys the **ProwlerScan Identity and Access Management (IAM) role** in your management account and in every member account. The wizard can deploy both from a **single CloudFormation stack** ([Step 4](#step-4-authenticate-with-your-management-account)), or you can deploy them yourself beforehand using Steps 1–2. The onboarding follows this sequence:
|
||||
<VersionBadge version="5.35.0" />
|
||||
|
||||
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).
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/onboarding-flow.svg" alt="Onboarding flow: 1. Start the Wizard, 2. Deploy the Roles (single CloudFormation stack), 3. Discover and Connect, 4. Launch Scans" />
|
||||
</Frame>
|
||||
|
||||
## 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. | By the wizard's **single stack** ([Step 4](#step-4-authenticate-with-your-management-account)), or on its own 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. | By the wizard's **single stack** ([Step 4](#step-4-authenticate-with-your-management-account)), or on its own via a **CloudFormation StackSet** ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Automated across all accounts. |
|
||||
|
||||
<Frame caption="Both roles share the same name `ProwlerScan`. The management account role includes additional Organization discovery permissions.">
|
||||
<img src="/images/organizations/two-roles-architecture.svg" alt="Two Roles Architecture: ProwlerScan in management account (Quick Create or Manual, discovery + scanning) and ProwlerScan in member accounts (via StackSet, scanning only)" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
**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).
|
||||
</Note>
|
||||
|
||||
### 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.
|
||||
|
||||
<Warning>
|
||||
**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. The Prowler wizard works around this by having the **same** stack create the management account role (`DeployLocalRole=true`) alongside the StackSet, so a single deployment covers both. You can also create the management account role on its own via the Quick Create link ([Option A](#option-a-quick-create-link-fastest)) or manually ([Option B](#option-b-create-the-role-manually)).
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
**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.
|
||||
</Note>
|
||||
|
||||
### 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.
|
||||
|
||||
<Tip>
|
||||
**[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)).
|
||||
</Tip>
|
||||
|
||||
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": "<YOUR_EXTERNAL_ID>"
|
||||
},
|
||||
"StringLike": {
|
||||
"aws:PrincipalArn": "arn:aws:iam::232136659152:role/prowler*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<YOUR_EXTERNAL_ID>` 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": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
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::<account-id>:role/ProwlerScan`
|
||||
|
||||
<Warning>
|
||||
The role **must** be named `ProwlerScan`. Do not use a different name.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
## Step 2: Deploy the CloudFormation StackSet
|
||||
|
||||
This step deploys the **ProwlerScan** role to your member accounts using a CloudFormation StackSet. It is the **manual alternative** to letting the wizard's single stack create the StackSet for you ([Step 4](#step-4-authenticate-with-your-management-account)) — use it if you prefer to create and manage the StackSet yourself.
|
||||
|
||||
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.
|
||||
|
||||
<Note>
|
||||
**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**.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
**The Prowler wizard now deploys this StackSet for you.** The **Create Stack in Management Account** button ([Step 4](#step-4-authenticate-with-your-management-account)) launches a single CloudFormation Stack that creates the management account role **and** a service-managed StackSet for your member accounts (`DeployStackSet=true`). Use the manual console steps below only if you prefer to create the StackSet yourself.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
**[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.
|
||||
</Tip>
|
||||
|
||||
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.
|
||||
|
||||
<Note>
|
||||
**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.
|
||||
</Note>
|
||||
|
||||
### 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
|
||||
|
||||
Start the Prowler wizard. It walks you through deploying both roles — the management account role and the ProwlerScan role in member accounts — from a single CloudFormation stack ([Step 4](#step-4-authenticate-with-your-management-account)). If you already deployed them beforehand via Steps 1–2, the wizard simply picks up where you left off.
|
||||
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,13 +74,13 @@ Start the Prowler wizard. It walks you through deploying both roles — the mana
|
||||
|
||||
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 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.
|
||||
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. 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.
|
||||
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
|
||||
|
||||
@@ -294,12 +88,20 @@ The wizard displays a **Prowler External ID** at the top — auto-generated and
|
||||
|
||||
The wizard deploys the deployment account role and the member-account StackSet in a **single** CloudFormation Stack:
|
||||
|
||||
<Note>
|
||||
**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).
|
||||
</Note>
|
||||
|
||||
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.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/authentication-details.png" alt="Authentication Details form showing External ID, Organizational Unit or Root ID field, delegated administrator checkbox, deployment account stack button, deployment account Role ARN field, and deployment confirmation checkbox" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
**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:
|
||||
|
||||
@@ -311,17 +113,11 @@ aws organizations list-roots --query 'Roots[0].Id' --output text
|
||||
aws organizations list-organizational-units-for-parent --parent-id r-xxxx \
|
||||
--query 'OrganizationalUnits[].{Name:Name,Id:Id}' --output table
|
||||
```
|
||||
|
||||
If you deploy the CloudFormation template manually (instead of via the wizard link), set **`DeployStackSet=true`**, **`DeployLocalRole=true`**, and **`EnableOrganizations=true`**, then put the root/OU ID above into **`AWSOrganizationalUnitId`** (required whenever `DeployStackSet=true`). Leave **`DeployFromDelegatedAdmin=false`** unless you launch the stack from a delegated administrator account.
|
||||
</Tip>
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/authentication-details.png" alt="Authentication Details form showing External ID, Organizational Unit or Root ID field, delegated administrator checkbox, deployment account stack button, deployment account Role ARN field, and deployment confirmation checkbox" />
|
||||
</Frame>
|
||||
|
||||
### Enter the Deployment Account Role ARN
|
||||
|
||||
Paste the **Role ARN** created by the single stack above into the **Management Account Role ARN** field or, when delegated administrator mode is selected, the **Delegated Administrator Account Role ARN** field. If you deployed the management account role beforehand, use the role created in [Step 1](#step-1-create-the-management-account-role).
|
||||
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:
|
||||
```
|
||||
@@ -334,6 +130,10 @@ For example: `arn:aws:iam::123456789012:role/ProwlerScan`
|
||||
<img src="/images/organizations/role-arn-field.png" alt="Deployment account Role ARN field in the Authentication Details form" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
### Confirm and Discover
|
||||
|
||||
1. Check the box: **"The Stack has been successfully deployed in AWS"**.
|
||||
@@ -344,7 +144,7 @@ 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
|
||||
## Step 3: Select Accounts to Scan
|
||||
|
||||
### Understanding the Tree View
|
||||
|
||||
@@ -355,6 +155,7 @@ Once discovery completes, the wizard displays a **hierarchical tree view** of yo
|
||||
</Frame>
|
||||
|
||||
- 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.
|
||||
@@ -371,14 +172,12 @@ Only **ACTIVE** accounts can be selected for scanning:
|
||||
| **CLOSED** | No | Account has been closed. |
|
||||
|
||||
<Note>
|
||||
**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.
|
||||
</Note>
|
||||
|
||||
### Custom Aliases
|
||||
@@ -387,14 +186,9 @@ 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 may appear as **blocked** (grayed out, not selectable) when the account is **already linked to a different organization** in Prowler (`linked_to_other_organization`). Hover over the blocked account to see the specific reason.
|
||||
|
||||
Hover over the blocked account to see the specific reason.
|
||||
|
||||
## Step 6: Test Connections
|
||||
|
||||
### How Connection Testing Works
|
||||
## Step 4: Test Connections
|
||||
|
||||
Click **Test Connections** to verify that Prowler can assume the **ProwlerScan** role in each selected member account.
|
||||
|
||||
@@ -402,154 +196,225 @@ Click **Test Connections** to verify that Prowler can assume the **ProwlerScan**
|
||||
<img src="/images/organizations/test-connections.png" alt="Connection testing in progress with spinners on each account" />
|
||||
</Frame>
|
||||
|
||||
- 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."** 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.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/test-connections-button.png" alt="Test Connections button" />
|
||||
</Frame>
|
||||
|
||||
**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.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/connection-failures-skip.png" alt="Connection test results showing failed accounts with error banner and Skip Connection Validation button" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
**Skip Connection Validation** is only available when at least one account connected successfully.
|
||||
</Note>
|
||||
If **no accounts** connected successfully, 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.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/organizations/launch-scan.png" alt="Launch Scan step showing Accounts Connected confirmation, scan schedule selector, and Launch scan button" />
|
||||
</Frame>
|
||||
|
||||
### 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.
|
||||
|
||||
## 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](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/<role-name>`
|
||||
- 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 egress IPs](/security/networking)
|
||||
|
||||
### 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.
|
||||
|
||||
<Note>
|
||||
**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.
|
||||
</Note>
|
||||
|
||||
### 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 `<YOUR_EXTERNAL_ID>` 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": "<YOUR_EXTERNAL_ID>"
|
||||
},
|
||||
"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": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
6. Name the role **`ProwlerScan`** and click **Create role**. The ARN follows the format `arn:aws:iam::<account-id>: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.
|
||||
|
||||
<Note>
|
||||
**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**.
|
||||
</Note>
|
||||
|
||||
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.
|
||||
|
||||
<Frame caption="Both roles share the same name `ProwlerScan`. The management account role includes additional Organization discovery permissions.">
|
||||
<img src="/images/organizations/two-roles-architecture.svg" alt="Two Roles Architecture: ProwlerScan in management account (discovery + scanning) and ProwlerScan in member accounts (via StackSet, scanning only)" />
|
||||
</Frame>
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Core Prowler tool namespace from the `prowler_app_*` prefix to `prowler_*`
|
||||
@@ -4,6 +4,18 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Jira tenant information requests validate site names and do not follow redirects
|
||||
@@ -1 +0,0 @@
|
||||
`excluded_checks` and `excluded_services` in scan configurations to narrow the execution scope
|
||||
@@ -49,7 +49,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.35.0"
|
||||
prowler_version = "5.36.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"
|
||||
|
||||
@@ -27,11 +27,14 @@ the UI can validate live with `ajv`. This module provides:
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -39,6 +42,18 @@ from prowler.config.schema.registry import SCHEMAS
|
||||
_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:
|
||||
"""Render a Pydantic error location as a dot-separated path.
|
||||
|
||||
@@ -79,9 +94,9 @@ def validate_and_normalize_scan_config(
|
||||
sections and unknown keys inside registered sections are preserved
|
||||
untouched for forward compatibility with plugin-provided keys.
|
||||
- ``errors`` is a list of ``{"path": <dotted-path>, "message": <str>}``
|
||||
entries — one per Pydantic violation. When any error is present the
|
||||
normalized dictionary is returned empty so the caller never
|
||||
persists a partially validated configuration.
|
||||
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.
|
||||
"""
|
||||
@@ -156,6 +171,35 @@ def validate_and_normalize_scan_config(
|
||||
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:
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
version = "5.35.0"
|
||||
version = "5.36.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -8,15 +8,28 @@ the backend can persist verbatim in a Django ``JSONField``.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from prowler.config.scan_config_schema import (
|
||||
_get_provider_check_ids,
|
||||
_get_provider_services,
|
||||
validate_and_normalize_scan_config,
|
||||
validate_scan_config,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_provider_catalog_caches():
|
||||
"""Keep provider catalog cache state isolated between tests."""
|
||||
_get_provider_check_ids.cache_clear()
|
||||
_get_provider_services.cache_clear()
|
||||
yield
|
||||
_get_provider_check_ids.cache_clear()
|
||||
_get_provider_services.cache_clear()
|
||||
|
||||
|
||||
class Test_Non_Dict_Root:
|
||||
@pytest.mark.parametrize("payload", [None, "string", 42, [], (1, 2)])
|
||||
def test_non_mapping_root_is_rejected(self, payload):
|
||||
@@ -39,7 +52,7 @@ class Test_Success_Path:
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{
|
||||
"aws": {
|
||||
"excluded_checks": [" s3_bucket_public_access "],
|
||||
"excluded_checks": [" s3_bucket_default_encryption "],
|
||||
"excluded_services": [" s3 "],
|
||||
}
|
||||
}
|
||||
@@ -47,7 +60,7 @@ class Test_Success_Path:
|
||||
assert errors == []
|
||||
assert normalized == {
|
||||
"aws": {
|
||||
"excluded_checks": ["s3_bucket_public_access"],
|
||||
"excluded_checks": ["s3_bucket_default_encryption"],
|
||||
"excluded_services": ["s3"],
|
||||
}
|
||||
}
|
||||
@@ -62,6 +75,67 @@ class Test_Success_Path:
|
||||
assert errors == []
|
||||
assert normalized == {"aws": {"plugin_option": "preserved", "another": 42}}
|
||||
|
||||
def test_plugin_catalog_identifiers_are_accepted_and_catalogs_are_cached(self):
|
||||
payload = {
|
||||
"aws": {
|
||||
"excluded_checks": ["plugin_check"],
|
||||
"excluded_services": ["plugin_service"],
|
||||
}
|
||||
}
|
||||
with (
|
||||
patch(
|
||||
"prowler.config.scan_config_schema.CheckMetadata.get_bulk",
|
||||
return_value={"plugin_check": object()},
|
||||
) as check_catalog,
|
||||
patch(
|
||||
"prowler.config.scan_config_schema.list_services",
|
||||
return_value=["plugin_service"],
|
||||
) as service_catalog,
|
||||
):
|
||||
first_result = validate_and_normalize_scan_config(payload)
|
||||
second_result = validate_and_normalize_scan_config(payload)
|
||||
|
||||
normalized, errors = first_result
|
||||
assert errors == []
|
||||
assert normalized == {
|
||||
"aws": {
|
||||
"excluded_checks": ["plugin_check"],
|
||||
"excluded_services": ["plugin_service"],
|
||||
}
|
||||
}
|
||||
assert second_result == first_result
|
||||
check_catalog.assert_called_once_with("aws")
|
||||
service_catalog.assert_called_once_with("aws")
|
||||
|
||||
def test_catalog_caches_are_keyed_by_provider(self):
|
||||
with (
|
||||
patch(
|
||||
"prowler.config.scan_config_schema.CheckMetadata.get_bulk",
|
||||
side_effect=lambda provider: {f"{provider}_plugin_check": object()},
|
||||
) as check_catalog,
|
||||
patch(
|
||||
"prowler.config.scan_config_schema.list_services",
|
||||
side_effect=lambda provider: [f"{provider}_plugin_service"],
|
||||
) as service_catalog,
|
||||
):
|
||||
payload = {
|
||||
"aws": {
|
||||
"excluded_checks": ["aws_plugin_check"],
|
||||
"excluded_services": ["aws_plugin_service"],
|
||||
},
|
||||
"azure": {
|
||||
"excluded_checks": ["azure_plugin_check"],
|
||||
"excluded_services": ["azure_plugin_service"],
|
||||
},
|
||||
}
|
||||
first_result = validate_and_normalize_scan_config(payload)
|
||||
second_result = validate_and_normalize_scan_config(payload)
|
||||
|
||||
assert first_result[1] == []
|
||||
assert second_result == first_result
|
||||
assert check_catalog.call_args_list == [call("aws"), call("azure")]
|
||||
assert service_catalog.call_args_list == [call("aws"), call("azure")]
|
||||
|
||||
def test_omitted_defaults_are_not_injected(self):
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{"aws": {"max_ec2_instance_age_in_days": 90}}
|
||||
@@ -103,6 +177,89 @@ class Test_Success_Path:
|
||||
|
||||
|
||||
class Test_Error_Path:
|
||||
def test_unknown_excluded_check_is_rejected(self):
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{"aws": {"excluded_checks": ["aws_check_that_does_not_exist"]}}
|
||||
)
|
||||
assert normalized == {}
|
||||
assert errors == [
|
||||
{
|
||||
"path": "aws.excluded_checks[0]",
|
||||
"message": (
|
||||
"Unknown check 'aws_check_that_does_not_exist' for provider "
|
||||
"'aws'."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
def test_unknown_excluded_service_is_rejected(self):
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{"aws": {"excluded_services": ["not_a_real_aws_service"]}}
|
||||
)
|
||||
assert normalized == {}
|
||||
assert errors == [
|
||||
{
|
||||
"path": "aws.excluded_services[0]",
|
||||
"message": (
|
||||
"Unknown service 'not_a_real_aws_service' for provider 'aws'."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
def test_multiple_unknown_exclusions_return_deterministic_errors(self):
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{
|
||||
"aws": {
|
||||
"excluded_checks": [
|
||||
"unknown_check_one",
|
||||
"s3_bucket_default_encryption",
|
||||
"unknown_check_two",
|
||||
],
|
||||
"excluded_services": [
|
||||
"unknown_service_one",
|
||||
"s3",
|
||||
"unknown_service_two",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert normalized == {}
|
||||
assert errors == [
|
||||
{
|
||||
"path": "aws.excluded_checks[0]",
|
||||
"message": "Unknown check 'unknown_check_one' for provider 'aws'.",
|
||||
},
|
||||
{
|
||||
"path": "aws.excluded_checks[2]",
|
||||
"message": "Unknown check 'unknown_check_two' for provider 'aws'.",
|
||||
},
|
||||
{
|
||||
"path": "aws.excluded_services[0]",
|
||||
"message": (
|
||||
"Unknown service 'unknown_service_one' for provider 'aws'."
|
||||
),
|
||||
},
|
||||
{
|
||||
"path": "aws.excluded_services[2]",
|
||||
"message": (
|
||||
"Unknown service 'unknown_service_two' for provider 'aws'."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
def test_check_from_another_provider_is_rejected(self):
|
||||
azure_check = "postgresql_flexible_server_allow_access_services_disabled"
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{"aws": {"excluded_checks": [azure_check]}}
|
||||
)
|
||||
assert normalized == {}
|
||||
assert errors == [
|
||||
{
|
||||
"path": "aws.excluded_checks[0]",
|
||||
"message": f"Unknown check '{azure_check}' for provider 'aws'.",
|
||||
}
|
||||
]
|
||||
|
||||
def test_invalid_input_returns_empty_normalized_and_errors(self):
|
||||
normalized, errors = validate_and_normalize_scan_config(
|
||||
{"aws": {"excluded_services": ["s3", " s3 "]}}
|
||||
@@ -215,6 +372,18 @@ class Test_Backward_Compatible_Wrapper:
|
||||
assert errors
|
||||
assert all(set(err) == {"path", "message"} for err in errors)
|
||||
|
||||
def test_unknown_exclusion_yields_the_semantic_error(self):
|
||||
assert validate_scan_config(
|
||||
{"aws": {"excluded_services": ["not_a_real_aws_service"]}}
|
||||
) == [
|
||||
{
|
||||
"path": "aws.excluded_services[0]",
|
||||
"message": (
|
||||
"Unknown service 'not_a_real_aws_service' for provider 'aws'."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
def test_non_mapping_root_matches_new_contract(self):
|
||||
assert validate_scan_config(None) == [
|
||||
{
|
||||
|
||||
@@ -4,6 +4,26 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
<!-- changelog: release notes start -->
|
||||
|
||||
## [1.35.0] (Prowler v5.35.0)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- AWS Organizations onboarding now deploys the management account role and the member-account StackSet from a single CloudFormation stack, replacing the manual StackSet console step [(#11927)](https://github.com/prowler-cloud/prowler/pull/11927)
|
||||
- Dynamic providers can now be renamed and deleted from the Providers table [(#11957)](https://github.com/prowler-cloud/prowler/pull/11957)
|
||||
- Sidebar navigation with grouped sections, clearer active states, and a responsive mobile overlay [(#11994)](https://github.com/prowler-cloud/prowler/pull/11994)
|
||||
- Core Prowler tools in Lighthouse use the `prowler_*` namespace while preserving legacy `prowler_app_*` compatibility [(#12017)](https://github.com/prowler-cloud/prowler/pull/12017)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- The AWS S3 integration CloudFormation quick-create link now sets the bucket owner account ID, preventing a stack validation error when S3 integration is enabled [(#11927)](https://github.com/prowler-cloud/prowler/pull/11927)
|
||||
- `Scan ID` filter on the Findings page now shows the active scan when opening findings from a scan's `View Findings` action [(#11997)](https://github.com/prowler-cloud/prowler/pull/11997)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- `js-yaml` to 4.3.0, `@sentry/nextjs` to 10.65.0 with `import-in-the-middle` 3.3.1, and transitive `hono`, `dompurify`, `ws`, `vite`, `@babel/core` and `@opentelemetry/core` to patched versions, resolving 13 npm audit advisories (3 high, 9 moderate, 1 low) plus `hono` CVE-2026-59896, published on NVD but not yet in the npm audit feed [(#12029)](https://github.com/prowler-cloud/prowler/pull/12029)
|
||||
|
||||
---
|
||||
|
||||
## [1.34.0] (Prowler v5.34.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
AWS Organizations onboarding now deploys the management account role and the member-account StackSet from a single CloudFormation stack, replacing the manual StackSet console step
|
||||
@@ -1 +0,0 @@
|
||||
Dynamic providers can now be renamed and deleted from the Providers table
|
||||
@@ -1 +0,0 @@
|
||||
`Scan ID` filter on the Findings page now shows the active scan when opening findings from a scan's `View Findings` action
|
||||
@@ -0,0 +1 @@
|
||||
OCI provider E2E tests no longer require or submit a region when adding or updating credentials
|
||||
@@ -1 +0,0 @@
|
||||
Core Prowler tools in Lighthouse use the `prowler_*` namespace while preserving legacy `prowler_app_*` compatibility
|
||||
@@ -1 +0,0 @@
|
||||
The AWS S3 integration CloudFormation quick-create link now sets the bucket owner account ID, preventing a stack validation error when S3 integration is enabled
|
||||
@@ -1 +0,0 @@
|
||||
Sidebar navigation with grouped sections, clearer active states, and a responsive mobile overlay
|
||||
@@ -1,8 +1,15 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { SocialButtons } from "./social-buttons";
|
||||
|
||||
// Stub Iconify: the real <Icon> fetches icon data over the network and its
|
||||
// retry timers can fire after the jsdom environment is torn down, crashing the
|
||||
// worker with "window is not defined".
|
||||
vi.mock("@iconify/react", () => ({
|
||||
Icon: ({ icon }: { icon: string }) => <span aria-label={icon} />,
|
||||
}));
|
||||
|
||||
describe("SocialButtons", () => {
|
||||
it("renders icon-only provider links that keep their accessible names", () => {
|
||||
render(
|
||||
|
||||
@@ -115,7 +115,7 @@ describe("OrgSetupForm", () => {
|
||||
|
||||
expect(params.get("param_DeployFromDelegatedAdmin")).toBe("true");
|
||||
expect(
|
||||
screen.getByLabelText("Delegated Administrator Account Role ARN"),
|
||||
screen.getByLabelText("Delegated Administrator Account IAM Role ARN"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ const orgSetupSchema = z.object({
|
||||
roleArn: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Role ARN is required")
|
||||
.min(1, "IAM Role ARN is required")
|
||||
.regex(
|
||||
/^arn:aws:iam::\d{12}:role\//,
|
||||
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan)",
|
||||
@@ -400,7 +400,7 @@ export function OrgSetupForm({
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
|
||||
1) Choose the AWS <strong>Organizational Unit</strong> (or root)
|
||||
to deploy to. Prowler creates the role in your deployment
|
||||
to deploy to. Prowler creates the IAM Role in your deployment
|
||||
account and rolls it out to every member account under this
|
||||
target.
|
||||
</p>
|
||||
@@ -422,7 +422,7 @@ export function OrgSetupForm({
|
||||
to the whole organization, or an <strong>OU ID</strong> (starts
|
||||
with <code>ou-</code>) to target a specific unit.
|
||||
</p>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Controller
|
||||
name="deployFromDelegatedAdmin"
|
||||
control={control}
|
||||
@@ -430,7 +430,7 @@ export function OrgSetupForm({
|
||||
<>
|
||||
<Checkbox
|
||||
id="deployFromDelegatedAdmin"
|
||||
className="mt-0.5"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
@@ -438,10 +438,10 @@ export function OrgSetupForm({
|
||||
/>
|
||||
<label
|
||||
htmlFor="deployFromDelegatedAdmin"
|
||||
className="text-text-neutral-tertiary text-xs leading-5 font-normal"
|
||||
className="text-text-neutral-secondary text-sm leading-5 font-medium"
|
||||
>
|
||||
I'm deploying from a delegated administrator
|
||||
account (not the Organization management account)
|
||||
I'm deploying from a Delegated Administrator
|
||||
Account (not the Management Account)
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
@@ -454,12 +454,12 @@ export function OrgSetupForm({
|
||||
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
|
||||
2) Create the CloudFormation Stack in your{" "}
|
||||
<strong>{deploymentAccountName}</strong>. It deploys the
|
||||
ProwlerScan role and a service-managed StackSet that rolls the
|
||||
role out to your member accounts in one step.
|
||||
ProwlerScan IAM Role and a service-managed StackSet that rolls
|
||||
the IAM Role out to your member accounts in one step.
|
||||
</p>
|
||||
{orgQuickLink ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="default"
|
||||
size="xl"
|
||||
className="w-full justify-start"
|
||||
asChild
|
||||
@@ -476,7 +476,7 @@ export function OrgSetupForm({
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="default"
|
||||
size="xl"
|
||||
className="w-full justify-start"
|
||||
disabled
|
||||
@@ -496,15 +496,15 @@ export function OrgSetupForm({
|
||||
{/* Step 3: Role ARN + confirm */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
|
||||
3) Paste the {deploymentAccountName} Role ARN and confirm the
|
||||
deployment is complete.
|
||||
3) Paste the {deploymentAccountName} IAM Role ARN and confirm
|
||||
the deployment is complete.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name="roleArn"
|
||||
label={`${deploymentAccountLabel} Role ARN`}
|
||||
label={`${deploymentAccountLabel} IAM Role ARN`}
|
||||
labelPlacement="outside"
|
||||
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerScan"
|
||||
isRequired={false}
|
||||
@@ -516,7 +516,7 @@ export function OrgSetupForm({
|
||||
ARN
|
||||
</p>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Controller
|
||||
name="stackSetDeployed"
|
||||
control={control}
|
||||
@@ -524,7 +524,7 @@ export function OrgSetupForm({
|
||||
<>
|
||||
<Checkbox
|
||||
id="stackSetDeployed"
|
||||
className="mt-0.5"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(Boolean(checked))
|
||||
|
||||
-10
@@ -48,16 +48,6 @@ export const OracleCloudCredentialsForm = ({
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OCI_REGION}
|
||||
type="text"
|
||||
label="Region"
|
||||
labelPlacement="inside"
|
||||
placeholder="e.g. us-ashburn-1"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<WizardTextareaField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OCI_KEY_CONTENT}
|
||||
|
||||
@@ -306,10 +306,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@sentry/nextjs",
|
||||
"from": "10.11.0",
|
||||
"to": "10.27.0",
|
||||
"from": "10.27.0",
|
||||
"to": "10.65.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-15T11:18:25.093Z"
|
||||
"generatedAt": "2026-07-16T15:35:58.334Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -435,17 +435,17 @@
|
||||
"section": "dependencies",
|
||||
"name": "import-in-the-middle",
|
||||
"from": "2.0.0",
|
||||
"to": "2.0.0",
|
||||
"to": "3.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-16T08:33:37.278Z"
|
||||
"generatedAt": "2026-07-16T15:35:58.334Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "js-yaml",
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.1",
|
||||
"from": "4.1.1",
|
||||
"to": "4.3.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-15T11:18:25.093Z"
|
||||
"generatedAt": "2026-07-16T15:29:57.887Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
|
||||
@@ -180,7 +180,6 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.OCI_FINGERPRINT]: "",
|
||||
[ProviderCredentialFields.OCI_KEY_CONTENT]: "",
|
||||
[ProviderCredentialFields.OCI_TENANCY]: providerUid || "",
|
||||
[ProviderCredentialFields.OCI_REGION]: "",
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]: "",
|
||||
};
|
||||
case "mongodbatlas":
|
||||
|
||||
@@ -389,10 +389,6 @@ export const buildOracleCloudSecret = (
|
||||
[ProviderCredentialFields.OCI_TENANCY]:
|
||||
providerUid ||
|
||||
getFormValue(formData, ProviderCredentialFields.OCI_TENANCY),
|
||||
[ProviderCredentialFields.OCI_REGION]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.OCI_REGION,
|
||||
),
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.OCI_PASS_PHRASE,
|
||||
|
||||
@@ -55,4 +55,43 @@ describe("validateYaml", () => {
|
||||
// Then
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
// Users author these documents by hand in the Mutelist and Scan Configuration
|
||||
// editors, so anchors, aliases and merge keys are legitimate input the syntax
|
||||
// check must keep accepting across js-yaml upgrades (4.3.0 rewrote merge-key
|
||||
// handling for CVE-2026-59869).
|
||||
it("accepts anchors, aliases and merge keys", () => {
|
||||
// When
|
||||
const result = validateYaml(
|
||||
[
|
||||
"defaults: &defaults",
|
||||
" max_unused_access_keys_days: 45",
|
||||
"aws:",
|
||||
" <<: *defaults",
|
||||
" max_console_access_days: 45",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a merge-key amplification document (CVE-2026-59869 shape)", () => {
|
||||
// Given — each mapping merges the previous one and adds a distinct key, so
|
||||
// merged-key copies grow quadratically (~45k total here). js-yaml 4.3.0
|
||||
// fixes the CVE by capping that work (maxTotalMergeKeys) and rejecting the
|
||||
// document; a vulnerable parser accepts it instead, turning the assertion
|
||||
// below red without relying on timing or suite timeouts.
|
||||
const chain = ["a0: &a0 { k0: 0 }"];
|
||||
for (let i = 1; i < 300; i++) {
|
||||
chain.push(`a${i}: &a${i} { <<: *a${i - 1}, k${i}: ${i} }`);
|
||||
}
|
||||
|
||||
// When
|
||||
const result = validateYaml(chain.join("\n"));
|
||||
|
||||
// Then
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toMatch(/merge keys/i);
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -73,7 +73,7 @@
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@react-stately/utils": "3.10.8",
|
||||
"@react-types/shared": "3.26.0",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@sentry/nextjs": "10.65.0",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
@@ -89,8 +89,8 @@
|
||||
"date-fns": "4.1.0",
|
||||
"driver.js": "1.4.0",
|
||||
"framer-motion": "11.18.2",
|
||||
"import-in-the-middle": "2.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"import-in-the-middle": "3.3.1",
|
||||
"js-yaml": "4.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"langchain": "1.4.0",
|
||||
"lucide-react": "0.543.0",
|
||||
|
||||
Generated
+690
-1320
File diff suppressed because it is too large
Load Diff
+33
-4
@@ -19,7 +19,13 @@ overrides:
|
||||
"@react-aria/interactions>react": "19.2.7"
|
||||
"lodash": "4.18.1"
|
||||
"lodash-es": "4.18.1"
|
||||
"hono": "4.12.21"
|
||||
# GHSA-88fw-hqm2-52qc (CORS reflects any Origin with credentials) + 4 moderate
|
||||
# advisories (serve-static path traversal, Lambda Set-Cookie merge, body-limit
|
||||
# bypass, Lambda@Edge repeated-header loss), all fixed in 4.12.25, plus
|
||||
# CVE-2026-59896 (hono/jsx SSR context leak across concurrent requests; in NVD
|
||||
# but not yet in the npm audit feed), fixed in 4.12.27. Not 4.12.29: it is
|
||||
# still inside StepSecurity's 7-day npm cooldown gate.
|
||||
"hono": "4.12.28"
|
||||
"@hono/node-server": "1.19.14"
|
||||
"@isaacs/brace-expansion": "5.0.1"
|
||||
"fast-xml-parser": "5.8.0"
|
||||
@@ -27,6 +33,29 @@ overrides:
|
||||
"postcss": "8.5.14"
|
||||
"esbuild": "0.28.1"
|
||||
"rollup@>=4": "4.59.0"
|
||||
# GHSA-fx2h-pf6j-xcff (server.fs.deny bypass on Windows alternate paths, high) +
|
||||
# GHSA-v6wh-96g9-6wx3 (launch-editor NTLMv2 hash disclosure). Dev-only tooling
|
||||
# (vitest/@vitejs/plugin-react); consumers allow ^7 but never resolve past 7.3.2
|
||||
# without a nudge.
|
||||
"vite@>=7 <8": "7.3.5"
|
||||
# GHSA-96hv-2xvq-fx4p (memory exhaustion DoS from tiny fragments, high) +
|
||||
# GHSA-58qx-3vcg-4xpx (uninitialized memory disclosure), both fixed in 8.21.0.
|
||||
# Not 8.21.1: it is still inside StepSecurity's 7-day npm cooldown gate.
|
||||
"ws@>=8 <9": "8.21.0"
|
||||
# Pulled by @sentry/server-utils bundler plugins (^2.1.0). Pinned because the
|
||||
# latest 2.3.1 is still inside StepSecurity's 7-day npm cooldown gate; safe to
|
||||
# drop this pin after 2026-07-20.
|
||||
"es-module-lexer": "2.3.0"
|
||||
# GHSA-4x5r-pxfx-6jf8 (arbitrary file read via sourceMappingURL comment, low),
|
||||
# fixed in 7.29.1. An override instead of `pnpm update` so the rest of the
|
||||
# babel/browserslist subtree keeps its existing lockfile resolutions.
|
||||
"@babel/core": "7.29.7"
|
||||
# Ephemeral cooldown pins: the @babel/helper-compilation-targets refresh pulls
|
||||
# browserslist-ecosystem releases newer than StepSecurity's 7-day npm cooldown.
|
||||
# Safe to drop after 2026-07-20.
|
||||
"browserslist": "4.28.2"
|
||||
"caniuse-lite": "1.0.30001792"
|
||||
"baseline-browser-mapping": "2.10.29"
|
||||
"minimatch@<4": "3.1.4"
|
||||
"minimatch@>=9 <10": "9.0.7"
|
||||
"minimatch@>=10": "10.2.3"
|
||||
@@ -42,9 +71,9 @@ overrides:
|
||||
# but the override unifies the tree on a patched version.
|
||||
"uuid": "11.1.1"
|
||||
# GHSA-vxr8-fq34-vvx9 (+ several related XSS sanitization bypasses): DOMPurify < 3.4.9,
|
||||
# pulled in transitively via streamdown > mermaid (which wants ^3.3.1). Pinned to 3.4.10
|
||||
# (fixes all open advisories; 3.4.11 is < 24h old and blocked by minimumReleaseAge).
|
||||
"dompurify": "3.4.10"
|
||||
# pulled in transitively via streamdown > mermaid (which wants ^3.3.1). Bumped to 3.4.11
|
||||
# for GHSA-cmwh-pvxp-8882 (permanent ALLOWED_ATTR pollution via setConfig()).
|
||||
"dompurify": "3.4.11"
|
||||
|
||||
# --- Level 1: Minimum Release Age ---
|
||||
# Packages must be published for at least 1 day before they can be installed.
|
||||
|
||||
@@ -224,7 +224,6 @@ export interface OCIProviderCredential {
|
||||
userId?: string;
|
||||
fingerprint?: string;
|
||||
keyContent?: string;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
// AlibabaCloud credential options
|
||||
@@ -366,7 +365,6 @@ export class ProvidersPage extends BasePage {
|
||||
readonly ociUserIdInput: Locator;
|
||||
readonly ociFingerprintInput: Locator;
|
||||
readonly ociKeyContentInput: Locator;
|
||||
readonly ociRegionInput: Locator;
|
||||
|
||||
// AlibabaCloud provider form elements
|
||||
readonly alibabacloudAccountIdInput: Locator;
|
||||
@@ -510,7 +508,6 @@ export class ProvidersPage extends BasePage {
|
||||
this.ociKeyContentInput = page.getByRole("textbox", {
|
||||
name: /Private Key Content/i,
|
||||
});
|
||||
this.ociRegionInput = page.getByRole("textbox", { name: /Region/i });
|
||||
|
||||
// AlibabaCloud provider form inputs
|
||||
this.alibabacloudAccountIdInput = page.getByRole("textbox", {
|
||||
@@ -1300,9 +1297,6 @@ export class ProvidersPage extends BasePage {
|
||||
if (credentials.keyContent) {
|
||||
await this.ociKeyContentInput.fill(credentials.keyContent);
|
||||
}
|
||||
if (credentials.region) {
|
||||
await this.ociRegionInput.fill(credentials.region);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyOCICredentialsPageLoaded(): Promise<void> {
|
||||
@@ -1313,7 +1307,6 @@ export class ProvidersPage extends BasePage {
|
||||
await expect(this.ociUserIdInput).toBeVisible();
|
||||
await expect(this.ociFingerprintInput).toBeVisible();
|
||||
await expect(this.ociKeyContentInput).toBeVisible();
|
||||
await expect(this.ociRegionInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOCIUpdateCredentialsPageLoaded(): Promise<void> {
|
||||
@@ -1324,7 +1317,6 @@ export class ProvidersPage extends BasePage {
|
||||
await expect(this.ociUserIdInput).toBeVisible();
|
||||
await expect(this.ociFingerprintInput).toBeVisible();
|
||||
await expect(this.ociKeyContentInput).toBeVisible();
|
||||
await expect(this.ociRegionInput).toBeVisible();
|
||||
}
|
||||
|
||||
async selectAlibabaCloudProvider(): Promise<void> {
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (admin.auth.setup setup)
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT
|
||||
- Remove any existing provider with the same Tenancy ID before starting the test
|
||||
- This test must be run serially and never in parallel with other tests, as it requires the Tenancy ID not to be already registered beforehand.
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
3. Select OCI provider type
|
||||
4. Fill provider details (tenancy ID and alias)
|
||||
5. Verify OCI credentials page is loaded
|
||||
6. Fill OCI credentials (user ID, fingerprint, key content, region)
|
||||
6. Fill OCI credentials (user ID, fingerprint, key content)
|
||||
7. Confirm provider connection without launching a scan
|
||||
8. Verify return to Providers page
|
||||
9. Verify provider exists in Providers table
|
||||
@@ -696,7 +696,7 @@
|
||||
- Connect account page displays OCI option
|
||||
- Provider details form accepts tenancy ID and alias
|
||||
- OCI credentials page loads
|
||||
- Credentials form accepts all required fields (user ID, fingerprint, key content, region)
|
||||
- Credentials form accepts all required fields (user ID, fingerprint, key content)
|
||||
- Launch step appears
|
||||
- Successful return to Providers page after closing the launch step
|
||||
- Provider exists in Providers table (verified by tenancy ID)
|
||||
@@ -726,7 +726,7 @@
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (admin.auth.setup setup)
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT
|
||||
- An OCI provider with the specified Tenancy ID must already exist (run PROVIDER-E2E-012 first)
|
||||
- This test must be run serially and never in parallel with other tests
|
||||
|
||||
@@ -738,7 +738,7 @@
|
||||
4. Click "Update Credentials" option
|
||||
5. Verify update credentials page is loaded
|
||||
6. Verify OCI credentials form fields are visible (confirms providerUid is loaded)
|
||||
7. Fill OCI credentials (user ID, fingerprint, key content, region)
|
||||
7. Fill OCI credentials (user ID, fingerprint, key content)
|
||||
8. Click Next to submit
|
||||
9. Verify successful navigation to test connection page
|
||||
|
||||
@@ -756,7 +756,7 @@
|
||||
- OCI provider row is visible in providers table
|
||||
- Row actions dropdown opens and displays "Update Credentials" option
|
||||
- Update credentials page URL contains correct parameters
|
||||
- OCI credentials form displays all fields (tenancy ID, user ID, fingerprint, key content, region)
|
||||
- OCI credentials form displays all required fields (tenancy ID, user ID, fingerprint, key content)
|
||||
- Form submission succeeds (no silent failures due to missing provider UID)
|
||||
- Successful redirect to test connection page
|
||||
|
||||
|
||||
@@ -1029,12 +1029,11 @@ test.describe("Add Provider", () => {
|
||||
const userId = process.env.E2E_OCI_USER_ID ?? "";
|
||||
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
|
||||
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
|
||||
const region = process.env.E2E_OCI_REGION ?? "";
|
||||
|
||||
// Setup before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.skip(
|
||||
!tenancyId || !userId || !fingerprint || !keyContent || !region,
|
||||
!tenancyId || !userId || !fingerprint || !keyContent,
|
||||
"OCI E2E env vars are not set",
|
||||
);
|
||||
providersPage = new ProvidersPage(page);
|
||||
@@ -1071,7 +1070,6 @@ test.describe("Add Provider", () => {
|
||||
userId: userId,
|
||||
fingerprint: fingerprint,
|
||||
keyContent: keyContent,
|
||||
region: region,
|
||||
};
|
||||
|
||||
// Navigate to providers page
|
||||
@@ -1516,12 +1514,11 @@ test.describe("Update Provider Credentials", () => {
|
||||
const userId = process.env.E2E_OCI_USER_ID ?? "";
|
||||
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
|
||||
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
|
||||
const region = process.env.E2E_OCI_REGION ?? "";
|
||||
|
||||
// Setup before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.skip(
|
||||
!tenancyId || !userId || !fingerprint || !keyContent || !region,
|
||||
!tenancyId || !userId || !fingerprint || !keyContent,
|
||||
"OCI E2E env vars are not set",
|
||||
);
|
||||
providersPage = new ProvidersPage(page);
|
||||
@@ -1543,7 +1540,6 @@ test.describe("Update Provider Credentials", () => {
|
||||
userId: userId,
|
||||
fingerprint: fingerprint,
|
||||
keyContent: keyContent,
|
||||
region: region,
|
||||
};
|
||||
|
||||
// Navigate to providers page
|
||||
|
||||
@@ -280,7 +280,6 @@ export type OCICredentials = {
|
||||
[ProviderCredentialFields.OCI_FINGERPRINT]: string;
|
||||
[ProviderCredentialFields.OCI_KEY_CONTENT]: string;
|
||||
[ProviderCredentialFields.OCI_TENANCY]: string;
|
||||
[ProviderCredentialFields.OCI_REGION]: string;
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]?: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
Vendored
-1
@@ -134,7 +134,6 @@ declare global {
|
||||
E2E_OCI_USER_ID?: string;
|
||||
E2E_OCI_FINGERPRINT?: string;
|
||||
E2E_OCI_KEY_CONTENT?: string;
|
||||
E2E_OCI_REGION?: string;
|
||||
|
||||
// E2E Alibaba Cloud
|
||||
E2E_ALIBABACLOUD_ACCOUNT_ID?: string;
|
||||
|
||||
@@ -220,3 +220,23 @@ users:
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addCredentialsFormSchema - oraclecloud", () => {
|
||||
const BASE_OCI_VALUES = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: "provider-oci-1",
|
||||
[ProviderCredentialFields.PROVIDER_TYPE]: "oraclecloud",
|
||||
[ProviderCredentialFields.OCI_USER]: "ocid1.user.oc1..example",
|
||||
[ProviderCredentialFields.OCI_FINGERPRINT]: "aa:bb:cc:dd",
|
||||
[ProviderCredentialFields.OCI_KEY_CONTENT]:
|
||||
"-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----",
|
||||
[ProviderCredentialFields.OCI_TENANCY]: "ocid1.tenancy.oc1..example",
|
||||
} as const;
|
||||
|
||||
it("accepts OCI API key credentials without region", () => {
|
||||
const schema = addCredentialsFormSchema("oraclecloud");
|
||||
|
||||
const result = schema.safeParse(BASE_OCI_VALUES);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -288,9 +288,6 @@ export const addCredentialsFormSchema = (
|
||||
[ProviderCredentialFields.OCI_TENANCY]: z
|
||||
.string()
|
||||
.min(1, "Tenancy OCID is required"),
|
||||
[ProviderCredentialFields.OCI_REGION]: z
|
||||
.string()
|
||||
.min(1, "Region is required"),
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]: z
|
||||
.union([z.string(), z.literal("")])
|
||||
.optional(),
|
||||
|
||||
Reference in New Issue
Block a user