fix(oci): scan identity in known valid region (#11056)

Co-authored-by: rchotacode <32524742+rchotacode@users.noreply.github.com>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
This commit is contained in:
Prowler Bot
2026-05-06 12:44:26 +02:00
committed by GitHub
parent 6b6d22bb31
commit bbe45ed708
6 changed files with 313 additions and 33 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.25.3] (Prowler UNRELEASED)
### 🐞 Fixed
- Oracle cloud identity scans now scan known or supplied regions to better support non ashburn tenancies [(#10528)](https://github.com/prowler-cloud/prowler/pull/10529)
---
## [5.25.2] (Prowler v5.25.2)
### 🐞 Fixed
@@ -66,6 +66,7 @@ class OraclecloudProvider(Provider):
_compartments: list = []
_mutelist: OCIMutelist
audit_metadata: Audit_Metadata
_home_region: str = "us-ashburn-1"
def __init__(
self,
@@ -160,6 +161,13 @@ class OraclecloudProvider(Provider):
# Get regions
self._regions = self.get_regions_to_audit(region)
self._home_region = None
if self._regions:
self._home_region = next(
(region.key for region in self._regions if region.is_home_region),
self._regions[0].key,
)
logger.info(f"Home region is: {self._home_region}")
# Get compartments
self._compartments = self.get_compartments_to_audit(
@@ -217,6 +225,10 @@ class OraclecloudProvider(Provider):
def regions(self):
return self._regions
@property
def home_region(self):
return self._home_region
@property
def compartments(self):
return self._compartments
@@ -1,6 +1,7 @@
"""OCI Identity Service Module."""
from datetime import datetime
from threading import Lock
from typing import Optional
import oci
@@ -26,6 +27,7 @@ class Identity(OCIService):
self.policies = []
self.dynamic_groups = []
self.domains = []
self._domains_lock = Lock()
self.password_policy = None
self.root_compartment_resources = []
self.active_non_root_compartments = []
@@ -61,8 +63,8 @@ class Identity(OCIService):
regional_client: Regional OCI client
"""
try:
# Identity is a global service, use home region
if regional_client.region not in self.provider.identity.region:
# Only use one region for global users
if regional_client.region != self.provider.home_region:
return
identity_client = self.__get_client__(regional_client.region)
@@ -312,7 +314,8 @@ class Identity(OCIService):
def __list_groups__(self, regional_client):
"""List all IAM groups."""
try:
if regional_client.region not in self.provider.identity.region:
# Only use one region for global groups
if regional_client.region != self.provider.home_region:
return
identity_client = self.__get_client__(regional_client.region)
@@ -355,7 +358,8 @@ class Identity(OCIService):
def __list_policies__(self, regional_client):
"""List all IAM policies."""
try:
if regional_client.region not in self.provider.identity.region:
# Only use one region for global policies
if regional_client.region != self.provider.home_region:
return
identity_client = self.__get_client__(regional_client.region)
@@ -399,8 +403,8 @@ class Identity(OCIService):
def __list_dynamic_groups__(self, regional_client):
"""List all dynamic groups in the tenancy."""
try:
# Dynamic groups are only in the home region
if regional_client.region not in self.provider.identity.region:
# Only use one region for global dynamic groups
if regional_client.region != self.provider.home_region:
return
identity_client = self.__get_client__(regional_client.region)
@@ -447,10 +451,6 @@ class Identity(OCIService):
def __list_domains__(self, regional_client):
"""List all identity domains."""
try:
# Domains are only in the home region
if regional_client.region not in self.provider.identity.region:
return
identity_client = self.__get_client__(regional_client.region)
logger.info("Identity - Listing Identity Domains...")
@@ -458,6 +458,7 @@ class Identity(OCIService):
try:
# List all domains in the tenancy
for compartment in self.audited_compartments:
domains = oci.pagination.list_call_get_all_results(
identity_client.list_domains,
compartment_id=compartment.id,
@@ -465,20 +466,38 @@ class Identity(OCIService):
).data
for domain in domains:
self.domains.append(
IdentityDomain(
id=domain.id,
display_name=domain.display_name,
description=domain.description or "",
url=domain.url,
home_region=domain.home_region,
compartment_id=compartment.id,
lifecycle_state=domain.lifecycle_state,
time_created=domain.time_created,
region=regional_client.region,
password_policies=[],
# Threads run __list_domains__ concurrently per
# region; serialize the dedupe-then-append so two
# regions returning the same domain cannot race
# past each other and produce duplicates or lose
# the home-region preference.
with self._domains_lock:
existing = next(
(d for d in self.domains if d.id == domain.id),
None,
)
if existing is not None:
# Prefer the entry from the domain's home region
if domain.home_region == regional_client.region:
self.domains.remove(existing)
else:
continue
self.domains.append(
IdentityDomain(
id=domain.id,
display_name=domain.display_name,
description=domain.description or "",
url=domain.url,
home_region=domain.home_region,
compartment_id=compartment.id,
lifecycle_state=domain.lifecycle_state,
time_created=domain.time_created,
region=regional_client.region,
password_policies=[],
)
)
)
except Exception as error:
logger.error(
@@ -493,8 +512,8 @@ class Identity(OCIService):
def __list_domain_password_policies__(self, regional_client):
"""List password policies for all identity domains."""
try:
# Password policies are only in the home region
if regional_client.region not in self.provider.identity.region:
# Only use one region for all domain scan
if regional_client.region != self.provider.home_region:
return
logger.info("Identity - Listing Domain Password Policies...")
@@ -551,7 +570,8 @@ class Identity(OCIService):
def __get_password_policy__(self, regional_client):
"""Get the password policy for the tenancy."""
try:
if regional_client.region not in self.provider.identity.region:
# Only use one region for global password policies
if regional_client.region != self.provider.home_region:
return
identity_client = self.__get_client__(regional_client.region)
@@ -578,8 +598,8 @@ class Identity(OCIService):
def __search_root_compartment_resources__(self, regional_client):
"""Search for resources in the root compartment using OCI Resource Search."""
try:
# Search is a global service, use home region
if regional_client.region not in self.provider.identity.region:
# Only use one region for global search
if regional_client.region != self.provider.home_region:
return
logger.info("Identity - Searching for resources in root compartment...")
@@ -626,10 +646,9 @@ class Identity(OCIService):
def __search_active_non_root_compartments__(self, regional_client):
"""Search for active non-root compartments using OCI Resource Search."""
try:
# Search is a global service, use home region
if regional_client.region not in self.provider.identity.region:
# Only use one region for global search
if regional_client.region != self.provider.home_region:
return
logger.info("Identity - Searching for active non-root compartments...")
# Create search client using the helper method for proper authentication
@@ -37,6 +37,7 @@ def set_mocked_oraclecloud_provider(
signer=MagicMock(),
profile="DEFAULT",
)
provider.home_region = region
# Mock identity
provider.identity = OCIIdentityInfo(
@@ -6,7 +6,7 @@ from prowler.providers.oraclecloud.exceptions.exceptions import (
OCIAuthenticationError,
OCIInvalidConfigError,
)
from prowler.providers.oraclecloud.models import OCISession
from prowler.providers.oraclecloud.models import OCIIdentityInfo, OCIRegion, OCISession
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
@@ -199,3 +199,59 @@ MIIEpQIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8n0sMcD/QHWCJ7yGSEtLN2T
)
assert connection.is_connected is True
class TestOraclecloudProviderInit:
"""Tests for OraclecloudProvider initialization"""
def test_init_with_region_set_populates_provider_state(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=[],
)
mock_regions = [
OCIRegion(key="us-phoenix-1", name="us-phoenix-1", is_home_region=False),
OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True),
]
mock_compartments = ["ocid1.compartment.oc1..aaaaaaaexample"]
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=mock_regions,
),
patch(
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_compartments_to_audit",
return_value=mock_compartments,
),
patch(
"prowler.providers.common.provider.Provider.set_global_provider"
) as mock_set_global,
):
provider = OraclecloudProvider(
region={"us-ashburn-1"},
config_content={"dummy": True},
mutelist_content={"Accounts": {}},
)
assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1"
assert provider.session == mock_session
assert provider.identity == mock_identity
assert provider.regions == mock_regions
assert provider.compartments == mock_compartments
assert provider.home_region == "us-ashburn-1"
mock_set_global.assert_called_once_with(provider)
@@ -1,4 +1,7 @@
from unittest.mock import patch
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from threading import Lock
from unittest.mock import MagicMock, patch
from tests.providers.oraclecloud.oci_fixtures import set_mocked_oraclecloud_provider
@@ -28,3 +31,184 @@ class TestIdentityService:
# Verify service name
assert identity_client.service == "identity"
assert identity_client.provider == oraclecloud_provider
def test_list_domains_passwords_skipped_outside_home(self):
"""Domains should be skipped when not in home region."""
with patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__",
return_value=None,
):
from prowler.providers.oraclecloud.services.identity.identity_service import (
Identity,
)
identity_client = Identity(None)
identity_client.service = "identity"
identity_client.provider = set_mocked_oraclecloud_provider()
identity_client.provider._home_region = "us-ashburn-1"
identity_client.audited_compartments = [
MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample")
]
identity_client.domains = []
identity_client._domains_lock = Lock()
identity_client.session_signer = None
identity_client.session_config = None
regional_client_ash = MagicMock()
regional_client_ash.region = "us-ashburn-1"
regional_client_chi = MagicMock()
regional_client_chi.region = "us-chicago-1"
policy = MagicMock()
policy.id = "123"
policy.name = "Test Policy"
policy.description = "This is a test policy"
policy.min_length = 8
policy.password_expires_after = 90
policy.num_passwords_in_history = 5
policy.password_expire_warning = 7
policy.min_password_age = 1
domains = []
for region in ["us-phoenix-1", "us-ashburn-1", "us-chicago-1"]:
domain = MagicMock()
domain.id = (
"ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID"
if region == "us-chicago-1"
else "ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID2"
)
domain.display_name = "exampledomain"
domain.description = "example"
domain.url = "https://idcs-example.identity.oraclecloud.com"
domain.home_region = region
domain.region = "us-ashburn-1"
domain.lifecycle_state = "ACTIVE"
domain.time_created = datetime.now()
domains.append(domain)
with (
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__",
return_value=MagicMock(),
),
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results",
return_value=MagicMock(data=domains),
),
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.oci.identity_domains.IdentityDomainsClient",
return_value=MagicMock(
list_password_policies=lambda: MagicMock(
data=MagicMock(resources=[policy])
)
),
),
):
identity_client.__list_domains__(regional_client_ash)
identity_client.__list_domains__(regional_client_chi)
identity_client.__list_domain_password_policies__(regional_client_ash)
identity_client.__list_domain_password_policies__(regional_client_chi)
assert (
len(identity_client.domains) == 2
and any(
domain.home_region == "us-ashburn-1"
and domain.region == "us-ashburn-1"
for domain in identity_client.domains
)
and any(
domain.home_region == "us-chicago-1"
and domain.region == "us-chicago-1"
for domain in identity_client.domains
)
and all(len(d.password_policies) == 1 for d in identity_client.domains)
)
def test_list_domains_concurrent_dedupes_and_prefers_home_region(self):
"""__list_domains__ runs across regions in parallel; the dedupe
must stay correct under concurrent calls (no duplicates, home
region wins)."""
with patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__",
return_value=None,
):
from prowler.providers.oraclecloud.services.identity.identity_service import (
Identity,
)
identity_client = Identity(None)
identity_client.service = "identity"
identity_client.provider = set_mocked_oraclecloud_provider()
identity_client.audited_compartments = [
MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample")
]
identity_client.domains = []
identity_client._domains_lock = Lock()
identity_client.session_signer = None
identity_client.session_config = None
regions = [
"us-ashburn-1",
"us-chicago-1",
"us-phoenix-1",
"eu-frankfurt-1",
]
home_region_by_domain = {
"ocid1.domain.oc1..domainA": "us-ashburn-1",
"ocid1.domain.oc1..domainB": "us-chicago-1",
"ocid1.domain.oc1..domainC": "eu-frankfurt-1",
}
# Each region returns the same set of domains (every domain
# is visible from every region; only one of those regions is
# actually the domain's home region).
def make_domains_for_region(_region):
ds = []
for domain_id, home_region in home_region_by_domain.items():
d = MagicMock()
d.id = domain_id
d.display_name = f"name-{domain_id}"
d.description = ""
d.url = "https://example.identity.oraclecloud.com"
d.home_region = home_region
d.lifecycle_state = "ACTIVE"
d.time_created = datetime.now()
ds.append(d)
return MagicMock(data=ds)
regional_clients = []
for region in regions:
rc = MagicMock()
rc.region = region
regional_clients.append(rc)
with (
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__",
return_value=MagicMock(),
),
patch(
"prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results",
side_effect=lambda _list_call, compartment_id, lifecycle_state: make_domains_for_region(
compartment_id
),
),
):
# Run several iterations to make any race more likely
# to surface; with the lock removed this loop fails
# frequently with duplicates.
for _ in range(20):
identity_client.domains = []
with ThreadPoolExecutor(
max_workers=len(regional_clients)
) as executor:
futures = [
executor.submit(identity_client.__list_domains__, rc)
for rc in regional_clients
]
for f in futures:
f.result()
assert len(identity_client.domains) == len(home_region_by_domain)
by_id = {d.id: d for d in identity_client.domains}
for domain_id, home_region in home_region_by_domain.items():
assert by_id[domain_id].region == home_region
assert by_id[domain_id].home_region == home_region