mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-03 17:54:05 +00:00
fix(oci): use home region for identity bootstrap (#12865)
This commit is contained in:
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
### Changes
|
||||
|
||||
This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` with the latest regions fetched from the OCI Identity API (`list_regions()`).
|
||||
This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` and the matching list in `ui/lib/provider-credentials/oci-regions.ts` with the latest regions fetched from the OCI Identity API (`list_regions()`).
|
||||
|
||||
- Government regions (`OCI_GOVERNMENT_REGIONS`) are preserved unchanged
|
||||
- DOD regions (`OCI_US_DOD_REGIONS`) are preserved unchanged
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
OCI provider secrets keep the region as home region for credential validation and scans, instead of always using us-ashburn-1
|
||||
@@ -215,36 +215,34 @@ class TestOracleCloudProviderSecret:
|
||||
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
|
||||
):
|
||||
def test_keeps_region_as_home_region(self):
|
||||
serializer = OracleCloudProviderSecret(
|
||||
data=self.valid_secret(**{legacy_field: legacy_value})
|
||||
data=self.valid_secret(region=" me-abudhabi-1 ")
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert serializer.validated_data["region"] == "me-abudhabi-1"
|
||||
|
||||
assert legacy_field not in serializer.validated_data
|
||||
def test_rejects_unknown_region(self):
|
||||
serializer = OracleCloudProviderSecret(
|
||||
data=self.valid_secret(region="mars-north-1")
|
||||
)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "region" in serializer.errors
|
||||
|
||||
@pytest.mark.parametrize("legacy_value", [None, "", {"name": "us-ashburn-1"}])
|
||||
def test_drops_blank_or_non_string_region(self, legacy_value):
|
||||
serializer = OracleCloudProviderSecret(
|
||||
data=self.valid_secret(region=legacy_value)
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert "region" not in serializer.validated_data
|
||||
|
||||
|
||||
class TestProviderSecretFieldSchema:
|
||||
def test_oraclecloud_schema_includes_legacy_region_field(self):
|
||||
def test_oraclecloud_schema_region_is_not_deprecated(self):
|
||||
schema = ProviderSecretField._spectacular_annotation["field"]
|
||||
oraclecloud_schema = next(
|
||||
credential_schema
|
||||
@@ -253,7 +251,7 @@ class TestProviderSecretFieldSchema:
|
||||
== "Oracle Cloud Infrastructure (OCI) API Key Credentials"
|
||||
)
|
||||
|
||||
assert oraclecloud_schema["properties"]["region"]["deprecated"] is True
|
||||
assert "deprecated" not in oraclecloud_schema["properties"]["region"]
|
||||
|
||||
|
||||
class TestKubernetesProviderSecret:
|
||||
|
||||
@@ -172,7 +172,7 @@ class TestInitializeProwlerProvider:
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_initialize_oraclecloud_provider_removes_region_string(
|
||||
def test_initialize_oraclecloud_provider_passes_region_as_home_region(
|
||||
self, mock_return_prowler_provider
|
||||
):
|
||||
provider = MagicMock()
|
||||
@@ -182,7 +182,7 @@ class TestInitializeProwlerProvider:
|
||||
"fingerprint": "00:11:22:33:44:55:66:77",
|
||||
"key_content": "fake-base64-key-content",
|
||||
"tenancy": "ocid1.tenancy.oc1..fake",
|
||||
"region": "us-ashburn-1",
|
||||
"region": "me-abudhabi-1",
|
||||
}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
@@ -193,6 +193,7 @@ class TestInitializeProwlerProvider:
|
||||
fingerprint="00:11:22:33:44:55:66:77",
|
||||
key_content="fake-base64-key-content",
|
||||
tenancy="ocid1.tenancy.oc1..fake",
|
||||
home_region="me-abudhabi-1",
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
@@ -254,11 +255,35 @@ class TestProwlerProviderConnectionTest:
|
||||
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,
|
||||
),
|
||||
region=OraclecloudProvider._bootstrap_region,
|
||||
provider_id="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_oraclecloud_connection_test_uses_stored_region_for_identity(
|
||||
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",
|
||||
"region": "me-abudhabi-1",
|
||||
}
|
||||
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="me-abudhabi-1",
|
||||
provider_id="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
@@ -434,7 +459,7 @@ class TestGetProwlerProviderKwargs:
|
||||
expected_result = {**secret_dict, **expected_extra_kwargs}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_oraclecloud_removes_region(
|
||||
def test_get_prowler_provider_kwargs_oraclecloud_maps_region_to_home_region(
|
||||
self,
|
||||
):
|
||||
secret_dict = {
|
||||
@@ -461,6 +486,7 @@ class TestGetProwlerProviderKwargs:
|
||||
"key_content": "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----",
|
||||
"tenancy": "ocid1.tenancy.oc1..fake",
|
||||
"pass_phrase": "fake-passphrase",
|
||||
"home_region": "us-ashburn-1",
|
||||
}
|
||||
|
||||
def test_get_prowler_provider_kwargs_with_mutelist(self):
|
||||
|
||||
@@ -3363,7 +3363,7 @@ current-context: test-context
|
||||
provider_secret = ProviderSecret.objects.get()
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
def test_provider_secrets_create_oraclecloud_accepts_and_ignores_region(
|
||||
def test_provider_secrets_create_oraclecloud_stores_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
@@ -3372,14 +3372,14 @@ current-context: test-context
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
self._oraclecloud_secret(
|
||||
key_content=" test-key-content ", region=" us-ashburn-1 "
|
||||
key_content=" test-key-content ", region=" me-abudhabi-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
|
||||
assert provider_secret.secret["region"] == "me-abudhabi-1"
|
||||
|
||||
def test_provider_secrets_update_oraclecloud_without_region_stores_no_region(
|
||||
self,
|
||||
@@ -3412,7 +3412,7 @@ current-context: test-context
|
||||
provider_secret.refresh_from_db()
|
||||
assert "region" not in provider_secret.secret
|
||||
|
||||
def test_provider_secrets_update_oraclecloud_accepts_and_ignores_region(
|
||||
def test_provider_secrets_update_oraclecloud_stores_region(
|
||||
self,
|
||||
authenticated_client,
|
||||
oraclecloud_provider,
|
||||
@@ -3430,7 +3430,7 @@ current-context: test-context
|
||||
"type": "provider-secrets",
|
||||
"id": str(provider_secret.id),
|
||||
"attributes": {
|
||||
"secret": self._oraclecloud_secret(region=" us-ashburn-1 ")
|
||||
"secret": self._oraclecloud_secret(region=" me-abudhabi-1 ")
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3443,7 +3443,7 @@ current-context: test-context
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
provider_secret.refresh_from_db()
|
||||
assert "region" not in provider_secret.secret
|
||||
assert provider_secret.secret["region"] == "me-abudhabi-1"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attributes, error_code, error_pointer",
|
||||
|
||||
@@ -302,17 +302,26 @@ def get_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)
|
||||
home_region = _oraclecloud_home_region(prowler_provider_kwargs.pop("region", None))
|
||||
if home_region:
|
||||
prowler_provider_kwargs["home_region"] = home_region
|
||||
|
||||
return prowler_provider_kwargs
|
||||
|
||||
|
||||
def _oraclecloud_home_region(region) -> str | None:
|
||||
"""Return the stored OCI region as a home region, ignoring blank or non-string legacy values."""
|
||||
if isinstance(region, str) and region.strip():
|
||||
return region.strip()
|
||||
return None
|
||||
|
||||
|
||||
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)
|
||||
home_region = _oraclecloud_home_region(prowler_provider_kwargs.pop("region", None))
|
||||
|
||||
if (
|
||||
prowler_provider_kwargs.get("user")
|
||||
@@ -323,11 +332,9 @@ def _normalize_oraclecloud_connection_test_kwargs(secret: dict) -> dict:
|
||||
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,
|
||||
# Identity calls only succeed in a region the tenancy is subscribed to.
|
||||
prowler_provider_kwargs["region"] = (
|
||||
home_region or OraclecloudProvider._bootstrap_region
|
||||
)
|
||||
|
||||
return prowler_provider_kwargs
|
||||
|
||||
@@ -301,8 +301,7 @@ from rest_framework_json_api import serializers
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"deprecated": True,
|
||||
"description": "Legacy OCI region field accepted for backwards compatibility but ignored; OCI scans all regions.",
|
||||
"description": "Optional OCI home region (or any region the tenancy is subscribed to) used to validate the credentials. It does not filter the scan, which covers all subscribed regions. Defaults to us-ashburn-1.",
|
||||
},
|
||||
},
|
||||
"required": ["user", "fingerprint", "tenancy"],
|
||||
|
||||
@@ -71,6 +71,7 @@ from django.db import IntegrityError, transaction
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from jwt.exceptions import InvalidKeyError
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.providers.oraclecloud.config import OCI_REGIONS
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from rest_framework_json_api import serializers
|
||||
@@ -1917,9 +1918,16 @@ class IacProviderSecret(serializers.Serializer):
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
|
||||
class LegacyOCIRegionField(serializers.Field):
|
||||
class OCIHomeRegionField(serializers.Field):
|
||||
"""Optional OCI home region; blank or non-string legacy values are dropped."""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
if not isinstance(data, str) or not data.strip():
|
||||
return None
|
||||
region = data.strip()
|
||||
if region not in OCI_REGIONS:
|
||||
raise serializers.ValidationError(f"Invalid OCI region: {region}")
|
||||
return region
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
@@ -1932,10 +1940,11 @@ class OracleCloudProviderSecret(serializers.Serializer):
|
||||
key_content = serializers.CharField(required=False)
|
||||
tenancy = serializers.CharField()
|
||||
pass_phrase = serializers.CharField(required=False)
|
||||
region = LegacyOCIRegionField(required=False, allow_null=True)
|
||||
region = OCIHomeRegionField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs.pop("region", None)
|
||||
if not attrs.get("region"):
|
||||
attrs.pop("region", None)
|
||||
|
||||
if "key_file" not in attrs and "key_content" not in attrs:
|
||||
raise serializers.ValidationError(
|
||||
|
||||
@@ -12,7 +12,7 @@ The following steps apply to Prowler Cloud and Prowler Local Server.
|
||||
1. Sign in to the [OCI Console](https://cloud.oracle.com/) and open **Tenancy Details** to copy the Tenancy OCID.
|
||||
2. Go to **Identity & Security** → **Users**, select the principal that owns the API key, and copy the **User OCID**.
|
||||
3. Generate or locate the API key fingerprint and private key for that user. Follow the [Config File Authentication steps](/user-guide/providers/oci/authentication#config-file-authentication-manual-api-key-setup) to create or rotate the key pair and copy the fingerprint.
|
||||
4. Note the **Region** identifier to scan (for example, `us-ashburn-1`).
|
||||
4. In **Tenancy Details**, note the **Home Region** identifier (for example, `me-abudhabi-1`). Any other region the tenancy is subscribed to also works.
|
||||
|
||||
### Step 2: Access Prowler Cloud
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler Local Server](/user-guide/tutorials/prowler-app).
|
||||
@@ -26,12 +26,18 @@ Prowler Cloud connects to OCI with API key credentials. Provide:
|
||||
|
||||
- **User OCID** for the API key owner
|
||||
- **Fingerprint** of the API key
|
||||
- **Region** (for example, `us-ashburn-1`)
|
||||
- **Home Region**: select it from the list (for example, `me-abudhabi-1`)
|
||||
- **Private Key Content** (paste the full PEM value)
|
||||
- **Passphrase (Optional)** if the private key is encrypted
|
||||
|
||||
Select **Next**, then **Launch Scan** to validate the connection and start the first OCI scan. The private key content is encoded for secure transmission.
|
||||
|
||||
<Note>
|
||||
The home region is used only to validate the credentials and discover the regions the tenancy is subscribed to. It does not limit the scan: Prowler audits every subscribed region. OCI Identity and Access Management (IAM) only answers in subscribed regions, so a tenancy that is not subscribed to the selected region cannot be validated.
|
||||
</Note>
|
||||
|
||||
Providers created without a region keep using `us-ashburn-1` for validation. If such a provider fails with `401 NotAuthenticated`, update its credentials and select the home region.
|
||||
|
||||

|
||||
|
||||
---
|
||||
@@ -334,6 +340,11 @@ prowler oci \
|
||||
|
||||
#### Region Issues
|
||||
|
||||
**Error: "OCI credential validation failed" with `401 NotAuthenticated` on `get_tenancy`**
|
||||
- The tenancy is not subscribed to the region used for validation (by default `us-ashburn-1`)
|
||||
- In Prowler Cloud or Prowler Local Server, update the provider credentials and select the tenancy home region
|
||||
- In Prowler CLI, set `region` in `~/.oci/config` to the home region. All subscribed regions are still scanned; `--region` also works but limits the scan to that region
|
||||
|
||||
**Error: "Invalid region"**
|
||||
- Check available regions: `prowler oci --list-regions`
|
||||
- Verify your tenancy is subscribed to the region
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
OCI API key credentials accept a home region to bootstrap identity calls, so tenancies not subscribed to us-ashburn-1 can connect
|
||||
@@ -89,6 +89,7 @@ class OraclecloudProvider(Provider):
|
||||
key_content: str = None,
|
||||
tenancy: str = None,
|
||||
pass_phrase: str = None,
|
||||
home_region: str = None,
|
||||
):
|
||||
"""
|
||||
Initializes the OCI provider.
|
||||
@@ -110,6 +111,7 @@ class OraclecloudProvider(Provider):
|
||||
- key_content: Content of the private key (base64 encoded).
|
||||
- tenancy: The OCID of the tenancy.
|
||||
- pass_phrase: The passphrase for the private key, if encrypted.
|
||||
- home_region: Region used to bootstrap identity calls with API key credentials; it does not filter the audited regions.
|
||||
|
||||
Raises:
|
||||
- OCISetUpSessionError: If an error occurs during the setup process.
|
||||
@@ -140,7 +142,7 @@ class OraclecloudProvider(Provider):
|
||||
)
|
||||
has_direct_credentials = user and fingerprint and tenancy
|
||||
bootstrap_region = single_region or (
|
||||
self._bootstrap_region if has_direct_credentials else None
|
||||
(home_region or self._bootstrap_region) if has_direct_credentials else None
|
||||
)
|
||||
|
||||
# Setup OCI Session
|
||||
|
||||
@@ -543,6 +543,58 @@ class TestOraclecloudProviderInit:
|
||||
assert mock_get_regions_to_audit.call_args_list[0].args == (None,)
|
||||
assert provider.regions == all_subscribed_regions
|
||||
|
||||
def test_init_with_home_region_bootstraps_there_without_scan_filter(self):
|
||||
mock_session = OCISession(
|
||||
config={"region": "me-abudhabi-1"}, signer=None, profile=None
|
||||
)
|
||||
mock_identity = OCIIdentityInfo(
|
||||
tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
tenancy_name="test-tenancy",
|
||||
user_id="ocid1.user.oc1..aaaaaaaexample",
|
||||
region="me-abudhabi-1",
|
||||
profile=None,
|
||||
audited_regions=set(),
|
||||
audited_compartments=[],
|
||||
)
|
||||
all_subscribed_regions = [
|
||||
OCIRegion(key="me-abudhabi-1", name="me-abudhabi-1", is_home_region=True),
|
||||
OCIRegion(key="me-dubai-1", name="me-dubai-1", is_home_region=False),
|
||||
]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.setup_session",
|
||||
return_value=mock_session,
|
||||
) as mock_setup_session,
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.set_identity",
|
||||
return_value=mock_identity,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit",
|
||||
return_value=all_subscribed_regions,
|
||||
) as mock_get_regions_to_audit,
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_compartments_to_audit",
|
||||
return_value=["ocid1.compartment.oc1..aaaaaaaexample"],
|
||||
),
|
||||
patch("prowler.providers.common.provider.Provider.set_global_provider"),
|
||||
):
|
||||
provider = OraclecloudProvider(
|
||||
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",
|
||||
home_region="me-abudhabi-1",
|
||||
config_content={"dummy": True},
|
||||
mutelist_content={"Accounts": {}},
|
||||
)
|
||||
|
||||
assert mock_setup_session.call_args.kwargs["region"] == "me-abudhabi-1"
|
||||
assert mock_get_regions_to_audit.call_args_list[0].args == (None,)
|
||||
assert provider.regions == all_subscribed_regions
|
||||
assert provider.home_region == "me-abudhabi-1"
|
||||
|
||||
def test_init_with_config_file_auth_without_region_uses_session_config_region_for_identity(
|
||||
self,
|
||||
):
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from prowler.providers.oraclecloud.config import OCI_REGIONS
|
||||
|
||||
UI_REGIONS_FILE = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "ui"
|
||||
/ "lib"
|
||||
/ "provider-credentials"
|
||||
/ "oci-regions.ts"
|
||||
)
|
||||
|
||||
|
||||
def test_ui_home_region_list_matches_sdk_regions():
|
||||
ui_regions = set(
|
||||
re.findall(r'"([a-z]{2,3}-[a-z-]+-\d+)"', UI_REGIONS_FILE.read_text())
|
||||
)
|
||||
|
||||
assert ui_regions == set(OCI_REGIONS)
|
||||
@@ -0,0 +1 @@
|
||||
Required home region selector in the OCI credentials form, so tenancies not subscribed to us-ashburn-1 can connect
|
||||
+31
@@ -4,6 +4,9 @@ import {
|
||||
WizardInputField,
|
||||
WizardTextareaField,
|
||||
} from "@/components/providers/workflow/forms/fields";
|
||||
import { Combobox } from "@/components/shadcn/combobox";
|
||||
import { FormControl, FormField, FormMessage } from "@/components/shadcn/form";
|
||||
import { OCI_REGION_GROUPS } from "@/lib/provider-credentials/oci-regions";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { OCICredentials } from "@/types";
|
||||
|
||||
@@ -48,6 +51,34 @@ export const OracleCloudCredentialsForm = ({
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OCI_REGION}
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-text-neutral-tertiary text-xs font-light tracking-tight">
|
||||
Home Region<span className="text-text-error-primary">*</span>
|
||||
</span>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
aria-label="Home Region"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
groups={OCI_REGION_GROUPS}
|
||||
placeholder="Select your tenancy home region"
|
||||
searchPlaceholder="Search region..."
|
||||
emptyMessage="No region found."
|
||||
contentClassName="z-[60] sm:w-(--radix-popover-trigger-width) sm:max-w-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<span className="text-text-neutral-tertiary text-xs">
|
||||
Shown in the OCI Console under Tenancy Details. Used only to
|
||||
validate the credentials: all subscribed regions are scanned.
|
||||
</span>
|
||||
<FormMessage className="text-text-error-primary max-w-full text-xs" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<WizardTextareaField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OCI_KEY_CONTENT}
|
||||
|
||||
@@ -181,6 +181,7 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.OCI_FINGERPRINT]: "",
|
||||
[ProviderCredentialFields.OCI_KEY_CONTENT]: "",
|
||||
[ProviderCredentialFields.OCI_TENANCY]: providerUid || "",
|
||||
[ProviderCredentialFields.OCI_REGION]: "",
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]: "",
|
||||
};
|
||||
case "mongodbatlas":
|
||||
|
||||
@@ -389,6 +389,10 @@ 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,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ComboboxGroup } from "@/components/shadcn/combobox";
|
||||
|
||||
// Keep in sync with OCI_REGIONS in prowler/providers/oraclecloud/config.py
|
||||
const OCI_COMMERCIAL_REGIONS = [
|
||||
"af-casablanca-1",
|
||||
"af-johannesburg-1",
|
||||
"ap-batam-1",
|
||||
"ap-chuncheon-1",
|
||||
"ap-hyderabad-1",
|
||||
"ap-kulai-2",
|
||||
"ap-melbourne-1",
|
||||
"ap-mumbai-1",
|
||||
"ap-osaka-1",
|
||||
"ap-seoul-1",
|
||||
"ap-singapore-1",
|
||||
"ap-singapore-2",
|
||||
"ap-sydney-1",
|
||||
"ap-tokyo-1",
|
||||
"ca-montreal-1",
|
||||
"ca-toronto-1",
|
||||
"eu-amsterdam-1",
|
||||
"eu-frankfurt-1",
|
||||
"eu-madrid-1",
|
||||
"eu-madrid-3",
|
||||
"eu-marseille-1",
|
||||
"eu-milan-1",
|
||||
"eu-paris-1",
|
||||
"eu-stockholm-1",
|
||||
"eu-turin-1",
|
||||
"eu-zurich-1",
|
||||
"il-jerusalem-1",
|
||||
"me-abudhabi-1",
|
||||
"me-dubai-1",
|
||||
"me-jeddah-1",
|
||||
"me-riyadh-1",
|
||||
"mx-monterrey-1",
|
||||
"mx-queretaro-1",
|
||||
"sa-bogota-1",
|
||||
"sa-santiago-1",
|
||||
"sa-saopaulo-1",
|
||||
"sa-valparaiso-1",
|
||||
"sa-vinhedo-1",
|
||||
"uk-cardiff-1",
|
||||
"uk-london-1",
|
||||
"us-ashburn-1",
|
||||
"us-chicago-1",
|
||||
"us-phoenix-1",
|
||||
"us-sanjose-1",
|
||||
];
|
||||
|
||||
const OCI_GOVERNMENT_REGIONS = [
|
||||
{ value: "us-langley-1", label: "us-langley-1 (US Gov West)" },
|
||||
{ value: "us-luke-1", label: "us-luke-1 (US Gov East)" },
|
||||
{ value: "us-gov-ashburn-1", label: "us-gov-ashburn-1 (US DoD East)" },
|
||||
{ value: "us-gov-chicago-1", label: "us-gov-chicago-1 (US DoD North)" },
|
||||
{ value: "us-gov-phoenix-1", label: "us-gov-phoenix-1 (US DoD West)" },
|
||||
];
|
||||
|
||||
export const OCI_REGION_GROUPS: ComboboxGroup[] = [
|
||||
{
|
||||
heading: "Commercial",
|
||||
options: OCI_COMMERCIAL_REGIONS.map((region) => ({
|
||||
value: region,
|
||||
label: region,
|
||||
})),
|
||||
},
|
||||
{ heading: "Government", options: OCI_GOVERNMENT_REGIONS },
|
||||
];
|
||||
|
||||
export const OCI_REGION_VALUES = OCI_REGION_GROUPS.flatMap((group) =>
|
||||
group.options.map((option) => option.value),
|
||||
);
|
||||
@@ -224,6 +224,7 @@ export interface OCIProviderCredential {
|
||||
userId?: string;
|
||||
fingerprint?: string;
|
||||
keyContent?: string;
|
||||
homeRegion?: string;
|
||||
}
|
||||
|
||||
// AlibabaCloud credential options
|
||||
@@ -365,6 +366,7 @@ export class ProvidersPage extends BasePage {
|
||||
readonly ociUserIdInput: Locator;
|
||||
readonly ociFingerprintInput: Locator;
|
||||
readonly ociKeyContentInput: Locator;
|
||||
readonly ociHomeRegionCombobox: Locator;
|
||||
|
||||
// AlibabaCloud provider form elements
|
||||
readonly alibabacloudAccountIdInput: Locator;
|
||||
@@ -510,6 +512,9 @@ export class ProvidersPage extends BasePage {
|
||||
this.ociKeyContentInput = page.getByRole("textbox", {
|
||||
name: /Private Key Content/i,
|
||||
});
|
||||
this.ociHomeRegionCombobox = page.getByRole("combobox", {
|
||||
name: /Home Region/i,
|
||||
});
|
||||
|
||||
// AlibabaCloud provider form inputs
|
||||
this.alibabacloudAccountIdInput = page.getByRole("textbox", {
|
||||
@@ -1284,6 +1289,12 @@ export class ProvidersPage extends BasePage {
|
||||
if (credentials.keyContent) {
|
||||
await this.ociKeyContentInput.fill(credentials.keyContent);
|
||||
}
|
||||
if (credentials.homeRegion) {
|
||||
await this.ociHomeRegionCombobox.click();
|
||||
await this.page
|
||||
.locator(`[role="option"][data-value="${credentials.homeRegion}"]`)
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyOCICredentialsPageLoaded(): Promise<void> {
|
||||
@@ -1294,6 +1305,7 @@ export class ProvidersPage extends BasePage {
|
||||
await expect(this.ociUserIdInput).toBeVisible();
|
||||
await expect(this.ociFingerprintInput).toBeVisible();
|
||||
await expect(this.ociKeyContentInput).toBeVisible();
|
||||
await expect(this.ociHomeRegionCombobox).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOCIUpdateCredentialsPageLoaded(): Promise<void> {
|
||||
@@ -1304,6 +1316,7 @@ export class ProvidersPage extends BasePage {
|
||||
await expect(this.ociUserIdInput).toBeVisible();
|
||||
await expect(this.ociFingerprintInput).toBeVisible();
|
||||
await expect(this.ociKeyContentInput).toBeVisible();
|
||||
await expect(this.ociHomeRegionCombobox).toBeVisible();
|
||||
}
|
||||
|
||||
async selectAlibabaCloudProvider(): Promise<void> {
|
||||
|
||||
@@ -611,7 +611,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
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION (optional, defaults to us-ashburn-1)
|
||||
- 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.
|
||||
|
||||
@@ -622,7 +622,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)
|
||||
6. Fill OCI credentials (user ID, fingerprint, key content, home region)
|
||||
7. Confirm provider connection without launching a scan
|
||||
8. Verify return to Providers page
|
||||
9. Verify provider exists in Providers table
|
||||
@@ -640,7 +640,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)
|
||||
- Credentials form accepts all required fields (user ID, fingerprint, key content, home region)
|
||||
- Launch step appears
|
||||
- Successful return to Providers page after closing the launch step
|
||||
- Provider exists in Providers table (verified by tenancy ID)
|
||||
@@ -670,7 +670,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
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION (optional, defaults to us-ashburn-1)
|
||||
- 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
|
||||
|
||||
@@ -682,7 +682,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)
|
||||
7. Fill OCI credentials (user ID, fingerprint, key content, home region)
|
||||
8. Click Next to submit
|
||||
9. Verify successful navigation to test connection page
|
||||
|
||||
|
||||
@@ -954,6 +954,7 @@ 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 homeRegion = process.env.E2E_OCI_REGION ?? "us-ashburn-1";
|
||||
|
||||
// Setup before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -995,6 +996,7 @@ test.describe("Add Provider", () => {
|
||||
userId: userId,
|
||||
fingerprint: fingerprint,
|
||||
keyContent: keyContent,
|
||||
homeRegion: homeRegion,
|
||||
};
|
||||
|
||||
// Navigate to providers page
|
||||
@@ -1439,6 +1441,7 @@ 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 homeRegion = process.env.E2E_OCI_REGION ?? "us-ashburn-1";
|
||||
|
||||
// Setup before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -1465,6 +1468,7 @@ test.describe("Update Provider Credentials", () => {
|
||||
userId: userId,
|
||||
fingerprint: fingerprint,
|
||||
keyContent: keyContent,
|
||||
homeRegion: homeRegion,
|
||||
};
|
||||
|
||||
// Navigate to providers page
|
||||
|
||||
@@ -281,6 +281,7 @@ 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
@@ -151,6 +151,7 @@ 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;
|
||||
|
||||
@@ -307,11 +307,33 @@ describe("addCredentialsFormSchema - oraclecloud", () => {
|
||||
[ProviderCredentialFields.OCI_TENANCY]: "ocid1.tenancy.oc1..example",
|
||||
} as const;
|
||||
|
||||
it("accepts OCI API key credentials without region", () => {
|
||||
it("rejects OCI API key credentials without a home region", () => {
|
||||
const schema = addCredentialsFormSchema("oraclecloud");
|
||||
|
||||
const result = schema.safeParse(BASE_OCI_VALUES);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects an unknown OCI home region", () => {
|
||||
const schema = addCredentialsFormSchema("oraclecloud");
|
||||
|
||||
const result = schema.safeParse({
|
||||
...BASE_OCI_VALUES,
|
||||
[ProviderCredentialFields.OCI_REGION]: "mars-north-1",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts OCI API key credentials with a home region", () => {
|
||||
const schema = addCredentialsFormSchema("oraclecloud");
|
||||
|
||||
const result = schema.safeParse({
|
||||
...BASE_OCI_VALUES,
|
||||
[ProviderCredentialFields.OCI_REGION]: "me-abudhabi-1",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import yaml from "js-yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OCI_REGION_VALUES } from "@/lib/provider-credentials/oci-regions";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { validateMutelistYaml, validateYaml } from "@/lib/yaml";
|
||||
import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS } from "@/types/saml";
|
||||
@@ -325,6 +326,14 @@ export const addCredentialsFormSchema = (
|
||||
[ProviderCredentialFields.OCI_TENANCY]: z
|
||||
.string()
|
||||
.min(1, "Tenancy OCID is required"),
|
||||
[ProviderCredentialFields.OCI_REGION]: z
|
||||
.string()
|
||||
.refine(
|
||||
(region) => OCI_REGION_VALUES.includes(region),
|
||||
{
|
||||
error: "Home region is required",
|
||||
},
|
||||
),
|
||||
[ProviderCredentialFields.OCI_PASS_PHRASE]: z
|
||||
.union([z.string(), z.literal("")])
|
||||
.optional(),
|
||||
|
||||
@@ -178,6 +178,34 @@ def update_config_file(regions, config_file_path):
|
||||
logging.info(f"Updated OCI_COMMERCIAL_REGIONS with {len(regions)} regions")
|
||||
|
||||
|
||||
def update_ui_regions_file(regions, ui_file_path):
|
||||
"""Rewrite OCI_COMMERCIAL_REGIONS in the UI region list used by the credentials form."""
|
||||
logging.info(f"Updating UI regions file: {ui_file_path}")
|
||||
|
||||
with open(ui_file_path, "r") as f:
|
||||
ui_content = f.read()
|
||||
|
||||
new_regions_array = "const OCI_COMMERCIAL_REGIONS = [\n"
|
||||
for region_id in regions.keys():
|
||||
new_regions_array += f' "{region_id}",\n'
|
||||
new_regions_array += "];"
|
||||
|
||||
pattern = r"const OCI_COMMERCIAL_REGIONS = \[[^\]]*\];"
|
||||
if not re.search(pattern, ui_content):
|
||||
raise Exception(
|
||||
"Validation failed: OCI_COMMERCIAL_REGIONS not found in the UI regions file."
|
||||
)
|
||||
updated_content = re.sub(pattern, new_regions_array, ui_content)
|
||||
|
||||
if updated_content == ui_content:
|
||||
logging.warning("No changes detected in UI regions file")
|
||||
return
|
||||
|
||||
with open(ui_file_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
logging.info("Successfully updated UI regions file")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main execution function for OCI regions updater.
|
||||
@@ -201,6 +229,16 @@ def main():
|
||||
|
||||
update_config_file(commercial_regions, config_file_path)
|
||||
|
||||
ui_file_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"..",
|
||||
"ui",
|
||||
"lib",
|
||||
"provider-credentials",
|
||||
"oci-regions.ts",
|
||||
)
|
||||
update_ui_regions_file(commercial_regions, ui_file_path)
|
||||
|
||||
logging.info("OCI regions update completed successfully")
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user