From 317f04de04feb04773c10db18ccdf1db1acb05bf Mon Sep 17 00:00:00 2001 From: "Hugo P.Brito" Date: Tue, 30 Jun 2026 14:44:43 +0100 Subject: [PATCH 1/3] feat(oraclecloud): support regionless SDK setup --- prowler/CHANGELOG.md | 4 + .../oraclecloud/oraclecloud_provider.py | 62 ++-- .../oraclecloud/oraclecloud_provider_test.py | 291 ++++++++++++++++++ 3 files changed, 338 insertions(+), 19 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 6e8306b9059..605bf37e584 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -126,6 +126,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - A broken built-in provider no longer aborts the CLI when a different provider was invoked [(#11618)](https://github.com/prowler-cloud/prowler/pull/11618) - GCP organization scans with `--organization-id` no longer silently fall back to the credentials' host project when the Cloud Asset API call fails [(#11280)](https://github.com/prowler-cloud/prowler/pull/11280) +### 🔄 Changed + +- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11565)](https://github.com/prowler-cloud/prowler/pull/11565) + --- ## [5.30.0] (Prowler v5.30.0) diff --git a/prowler/providers/oraclecloud/oraclecloud_provider.py b/prowler/providers/oraclecloud/oraclecloud_provider.py index b498bbb5029..9cd61c23b5e 100644 --- a/prowler/providers/oraclecloud/oraclecloud_provider.py +++ b/prowler/providers/oraclecloud/oraclecloud_provider.py @@ -68,12 +68,13 @@ class OraclecloudProvider(Provider): _mutelist: OCIMutelist audit_metadata: Audit_Metadata _home_region: str = "us-ashburn-1" + _bootstrap_region: str = "us-ashburn-1" def __init__( self, oci_config_file: str = None, profile: str = None, - region: set = set(), + region: set = None, compartment_ids: list = None, config_path: str = None, config_content: dict = None, @@ -131,8 +132,11 @@ def __init__( # Check if the configuration is scanning a single region single_region = None - if region: - single_region = list(region)[0] if len(region) == 1 else None + if isinstance(region, str): + single_region = region + elif region: + single_region = sorted(region)[0] + bootstrap_region = single_region or self._bootstrap_region # Setup OCI Session logger.info("Setting up OCI session ...") @@ -145,7 +149,7 @@ def __init__( key_file=key_file, key_content=key_content, tenancy=tenancy, - region=single_region, + region=bootstrap_region, pass_phrase=pass_phrase, ) @@ -155,7 +159,7 @@ def __init__( logger.info("Validating OCI credentials ...") self._identity = self.set_identity( session=self._session, - region=single_region, + region=bootstrap_region, compartment_ids=compartment_ids, ) logger.info("OCI credentials validated") @@ -165,7 +169,13 @@ def __init__( # Determine the tenancy home region from the full subscription list, independent of # the --region filter, so tenancy-level APIs (e.g. the Audit configuration) always # target the home region instead of a filtered, non-home region. - all_subscribed_regions = self.get_regions_to_audit() + try: + all_subscribed_regions = self.get_regions_to_audit() + except OCISetUpSessionError: + if single_region and len(self._regions) == 1: + all_subscribed_regions = self._regions + else: + raise self._home_region = next( (region.key for region in all_subscribed_regions if region.is_home_region), self._regions[0].key if self._regions else "us-ashburn-1", @@ -284,7 +294,7 @@ def setup_session( signer = None # If API key credentials are provided directly, create config from them - if user and fingerprint and tenancy and region: + if user and fingerprint and tenancy: import base64 logger.info("Using API key credentials from direct parameters") @@ -294,7 +304,7 @@ def setup_session( "user": user, "fingerprint": fingerprint, "tenancy": tenancy, - "region": region, + "region": region or OraclecloudProvider._bootstrap_region, } # Handle private key @@ -565,6 +575,12 @@ def get_regions_to_audit(self, region_set: set = None) -> list: """ regions = [] + explicit_regions = None + if isinstance(region_set, str): + explicit_regions = {region_set} + elif region_set: + explicit_regions = set(region_set) + # Audit all subscribed regions try: # Create identity client with proper authentication handling @@ -581,11 +597,9 @@ def get_regions_to_audit(self, region_set: set = None) -> list: ).data # Check if auditing specific region or all - regions_check = ( - region_set - if region_set - else [sub.region_name for sub in region_subscriptions] - ) + regions_check = explicit_regions or [ + sub.region_name for sub in region_subscriptions + ] for region_sub in region_subscriptions: if region_sub.region_name in regions_check: @@ -600,11 +614,21 @@ def get_regions_to_audit(self, region_set: set = None) -> list: ) logger.info(f"Found {len(regions)} subscribed regions") except Exception as error: + if not explicit_regions or len(explicit_regions) != 1: + raise OCISetUpSessionError( + original_exception=error, + message=( + "Could not retrieve OCI subscribed regions. " + "Configure an explicit region to preserve legacy single-region scans, " + "or fix the credentials/permissions required to list region subscriptions." + ), + ) from error + + config_region = next(iter(explicit_regions)) logger.warning( - f"Could not retrieve region subscriptions: {error}. Using configured region." + f"Could not retrieve region subscriptions: {error}. " + f"Using explicitly configured region {config_region}." ) - # Fallback to configured region - config_region = self._session.config.get("region", "us-ashburn-1") regions.append( OCIRegion( key=config_region, @@ -855,7 +879,7 @@ def test_connection( session = None # If API key credentials are provided directly, create config from them - if user and fingerprint and tenancy and region: + if user and fingerprint and tenancy: import base64 logger.info("Using API key credentials from direct parameters") @@ -865,7 +889,7 @@ def test_connection( "user": user, "fingerprint": fingerprint, "tenancy": tenancy, - "region": region, + "region": region or OraclecloudProvider._bootstrap_region, } # Handle private key @@ -914,7 +938,7 @@ def test_connection( identity = OraclecloudProvider.set_identity( session=session, - region=region, + region=region or OraclecloudProvider._bootstrap_region, ) # Validate provider_id if provided diff --git a/tests/providers/oraclecloud/oraclecloud_provider_test.py b/tests/providers/oraclecloud/oraclecloud_provider_test.py index 7c437a35ac1..f8773fc9ca2 100644 --- a/tests/providers/oraclecloud/oraclecloud_provider_test.py +++ b/tests/providers/oraclecloud/oraclecloud_provider_test.py @@ -5,6 +5,7 @@ from prowler.providers.oraclecloud.exceptions.exceptions import ( OCIAuthenticationError, OCIInvalidConfigError, + OCISetUpSessionError, ) from prowler.providers.oraclecloud.models import OCIIdentityInfo, OCIRegion, OCISession from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider @@ -200,6 +201,41 @@ def test_test_connection_valid_key_content_proceeds(self): assert connection.is_connected is True + def test_test_connection_direct_credentials_without_region_uses_bootstrap_region( + self, + ): + """Direct API key auth should not fall back to config-file auth without a region.""" + import base64 + + valid_key = ( + "-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----" + ) + encoded_key = base64.b64encode(valid_key.encode("utf-8")).decode("utf-8") + + with ( + patch("oci.config.validate_config") as mock_validate_config, + patch("oci.identity.IdentityClient") as mock_identity_client, + ): + mock_tenancy = MagicMock() + mock_tenancy.name = "test-tenancy" + mock_response = MagicMock() + mock_response.data = mock_tenancy + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.return_value = mock_response + mock_identity_client.return_value = mock_client_instance + + connection = OraclecloudProvider.test_connection( + key_content=encoded_key, + user="ocid1.user.oc1..aaaaaaaexample", + fingerprint="aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + tenancy="ocid1.tenancy.oc1..aaaaaaaexample", + provider_id="ocid1.tenancy.oc1..aaaaaaaexample", + raise_on_exception=False, + ) + + assert connection.is_connected is True + assert mock_validate_config.call_args.args[0]["region"] == "us-ashburn-1" + class TestOraclecloudProviderInit: """Tests for OraclecloudProvider initialization""" @@ -256,6 +292,159 @@ def test_init_with_region_set_populates_provider_state(self): assert provider.home_region == "us-ashburn-1" mock_set_global.assert_called_once_with(provider) + def test_init_with_multiple_regions_uses_deterministic_session_region(self): + mock_session = OCISession( + config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-ashburn-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + audited_regions = [ + OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True), + OCIRegion(key="us-phoenix-1", name="us-phoenix-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, + ) as mock_set_identity, + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit", + return_value=audited_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( + region={"us-phoenix-1", "us-ashburn-1"}, + 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", + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + + assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1" + assert mock_set_identity.call_args.kwargs["region"] == "us-ashburn-1" + assert mock_get_regions_to_audit.call_args_list[0].args == ( + {"us-phoenix-1", "us-ashburn-1"}, + ) + assert provider.regions == audited_regions + + def test_init_with_legacy_region_string_uses_full_region_for_identity(self): + mock_session = OCISession( + config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-ashburn-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + audited_regions = [ + OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True), + ] + + 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, + ) as mock_set_identity, + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit", + return_value=audited_regions, + ), + 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"), + ): + OraclecloudProvider( + region="us-ashburn-1", + 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", + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + + assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1" + assert mock_set_identity.call_args.kwargs["region"] == "us-ashburn-1" + + def test_init_without_region_uses_bootstrap_region_without_scan_filter(self): + mock_session = OCISession( + config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-ashburn-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + all_subscribed_regions = [ + OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True), + OCIRegion(key="us-phoenix-1", name="us-phoenix-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, + ) as mock_set_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", + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + + assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1" + assert mock_set_identity.call_args.kwargs["region"] == "us-ashburn-1" + assert mock_get_regions_to_audit.call_args_list[0].args == (None,) + assert provider.regions == all_subscribed_regions + def test_home_region_uses_full_subscription_list_not_region_filter(self): """Home region must come from the full subscription list, not the --region filter. @@ -313,3 +502,105 @@ def test_home_region_uses_full_subscription_list_not_region_filter(self): assert provider.regions == audited_regions assert provider.home_region == "us-ashburn-1" + + def test_init_with_legacy_single_region_preserves_fallback_for_home_region(self): + mock_session = OCISession( + config={"region": "us-phoenix-1"}, signer=None, profile="DEFAULT" + ) + mock_identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-phoenix-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + + with ( + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.setup_session", + return_value=mock_session, + ), + patch( + "prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.set_identity", + return_value=mock_identity, + ), + patch("oci.identity.IdentityClient") as mock_identity_client, + 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"), + ): + mock_identity_client.return_value.list_region_subscriptions.side_effect = ( + Exception("discovery failed") + ) + + provider = OraclecloudProvider( + region="us-phoenix-1", + config_content={"dummy": True}, + mutelist_content={"Accounts": {}}, + ) + + assert [region.key for region in provider.regions] == ["us-phoenix-1"] + assert provider.home_region == "us-phoenix-1" + + +class TestGetRegionsToAudit: + def _provider_with_identity(self): + provider = OraclecloudProvider.__new__(OraclecloudProvider) + provider._session = OCISession( + config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT" + ) + provider._identity = OCIIdentityInfo( + tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample", + tenancy_name="test-tenancy", + user_id="ocid1.user.oc1..aaaaaaaexample", + region="us-ashburn-1", + profile="DEFAULT", + audited_regions=set(), + audited_compartments=[], + ) + return provider + + def test_regionless_scan_raises_when_region_subscription_discovery_fails(self): + provider = self._provider_with_identity() + + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_identity_client.return_value.list_region_subscriptions.side_effect = ( + Exception("discovery failed") + ) + + with pytest.raises(OCISetUpSessionError) as exc_info: + provider.get_regions_to_audit() + + assert "Could not retrieve OCI subscribed regions" in str(exc_info.value) + + def test_single_explicit_region_falls_back_when_region_subscription_discovery_fails( + self, + ): + provider = self._provider_with_identity() + + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_identity_client.return_value.list_region_subscriptions.side_effect = ( + Exception("discovery failed") + ) + + regions = provider.get_regions_to_audit("us-phoenix-1") + + assert len(regions) == 1 + assert regions[0].key == "us-phoenix-1" + + def test_multiple_explicit_regions_raise_when_region_subscription_discovery_fails( + self, + ): + provider = self._provider_with_identity() + + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_identity_client.return_value.list_region_subscriptions.side_effect = ( + Exception("discovery failed") + ) + + with pytest.raises(OCISetUpSessionError): + provider.get_regions_to_audit({"us-ashburn-1", "us-phoenix-1"}) From 6bfcd745ba7abc3c05f0b31beb10cdb3475c05fb Mon Sep 17 00:00:00 2001 From: "Hugo P.Brito" Date: Tue, 30 Jun 2026 15:30:22 +0100 Subject: [PATCH 2/3] docs(changelog): move OCI SDK entry to 5.32 --- prowler/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 605bf37e584..c5368c511a8 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -21,6 +21,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - CIS Kubernetes Benchmark v2.0.1 compliance framework for the Kubernetes provider [(#11722)](https://github.com/prowler-cloud/prowler/pull/11722) - CIS GitHub Benchmark v1.2.0 compliance framework for the GitHub provider [(#11719)](https://github.com/prowler-cloud/prowler/pull/11719) +### 🔄 Changed + +- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11565)](https://github.com/prowler-cloud/prowler/pull/11565) + ### 🐞 Fixed - Compliance frameworks contributed by several external packages under the same provider are now merged instead of overwritten, so every entry-point directory a provider contributes is discovered [(#11578)](https://github.com/prowler-cloud/prowler/pull/11578) @@ -126,10 +130,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - A broken built-in provider no longer aborts the CLI when a different provider was invoked [(#11618)](https://github.com/prowler-cloud/prowler/pull/11618) - GCP organization scans with `--organization-id` no longer silently fall back to the credentials' host project when the Cloud Asset API call fails [(#11280)](https://github.com/prowler-cloud/prowler/pull/11280) -### 🔄 Changed - -- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11565)](https://github.com/prowler-cloud/prowler/pull/11565) - --- ## [5.30.0] (Prowler v5.30.0) From c1fe9fdf4ff2210a353543cb8fccf86b2a9d2cae Mon Sep 17 00:00:00 2001 From: "Hugo P.Brito" Date: Mon, 6 Jul 2026 12:48:14 +0100 Subject: [PATCH 3/3] docs(changelog): move OCI SDK entry to 5.33 --- prowler/CHANGELOG.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c5368c511a8..e0be7134a7a 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,7 +2,15 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [5.32.0] (Prowler UNRELEASED) +## [5.33.0] (Prowler UNRELEASED) + +### 🔄 Changed + +- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11740)](https://github.com/prowler-cloud/prowler/pull/11740) + +--- + +## [5.32.0] (Prowler v5.32.0) ### 🚀 Added @@ -21,10 +29,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - CIS Kubernetes Benchmark v2.0.1 compliance framework for the Kubernetes provider [(#11722)](https://github.com/prowler-cloud/prowler/pull/11722) - CIS GitHub Benchmark v1.2.0 compliance framework for the GitHub provider [(#11719)](https://github.com/prowler-cloud/prowler/pull/11719) -### 🔄 Changed - -- Oracle Cloud API key authentication now uses an internal bootstrap region when no explicit scan region filter is provided [(#11565)](https://github.com/prowler-cloud/prowler/pull/11565) - ### 🐞 Fixed - Compliance frameworks contributed by several external packages under the same provider are now merged instead of overwritten, so every entry-point directory a provider contributes is discovered [(#11578)](https://github.com/prowler-cloud/prowler/pull/11578)