mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user