From 5bfe996e95e253b6e0532f138a8fffb02b5490d2 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Wed, 25 Mar 2026 11:01:22 +0100 Subject: [PATCH] fix(oci): false positive for password policies (#10463) Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> --- prowler/CHANGELOG.md | 3 +- ...licy_expires_within_365_days.metadata.json | 2 +- ...ord_policy_minimum_length_14.metadata.json | 2 +- ...ssword_policy_prevents_reuse.metadata.json | 2 +- .../services/identity/identity_service.py | 8 + ...ord_policy_expires_within_365_days_test.py | 356 ++++++++-------- ..._password_policy_minimum_length_14_test.py | 332 ++++++++------- ...ity_password_policy_prevents_reuse_test.py | 382 ++++++++++-------- 8 files changed, 585 insertions(+), 502 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 7b78f83380..366581957a 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed -- Oracle Cloud `events_rule_idp_group_mapping_changes` now recognizes the CIS 3.1 `add/remove` event names to avoid false positives [(#10411)](https://github.com/prowler-cloud/prowler/issues/10411) +- Oracle Cloud `events_rule_idp_group_mapping_changes` now recognizes the CIS 3.1 `add/remove` event names to avoid false positives [(#10416)](https://github.com/prowler-cloud/prowler/pull/10416) +- Oracle Cloud password policy checks now exclude immutable system-managed policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) to avoid false positives [(#10453)](https://github.com/prowler-cloud/prowler/pull/10453) --- diff --git a/prowler/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days.metadata.json b/prowler/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days.metadata.json index 5d8cfbf196..809975308e 100644 --- a/prowler/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days.metadata.json +++ b/prowler/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "Policy", "ResourceGroup": "IAM", - "Description": "**OCI Identity Domain password policies** are evaluated to confirm **password expiration** is configured and set to `<= 365` days (`password_expires_after`).\n\n*Legacy IAM lacks password expiration; tenancies without Identity Domains require manual assessment.*", + "Description": "**OCI Identity Domain password policies** are evaluated to confirm **password expiration** is configured and set to `<= 365` days (`password_expires_after`). System-managed immutable policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) are excluded.\n\n*Legacy IAM lacks password expiration; tenancies without Identity Domains require manual assessment.*", "Risk": "Missing or >`365`-day **password expiration** extends the window for **credential stuffing**, **brute force**, and use of leaked passwords. This enables unauthorized access, data exposure, and configuration changes, harming **confidentiality** and **integrity**, and allowing attacker persistence after staff turnover.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14.metadata.json b/prowler/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14.metadata.json index 63c691b266..b8daacaa10 100644 --- a/prowler/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14.metadata.json +++ b/prowler/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14.metadata.json @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "Policy", "ResourceGroup": "IAM", - "Description": "**OCI IAM password policies** are evaluated to confirm a **minimum password length** of `>= 14` characters is enforced. The assessment considers policies defined in **Identity Domains** and the legacy tenancy policy, and also detects when no password policy exists.", + "Description": "**OCI IAM password policies** are evaluated to confirm a **minimum password length** of `>= 14` characters is enforced. The assessment considers policies defined in **Identity Domains** and the legacy tenancy policy, and also detects when no password policy exists. System-managed immutable policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) are excluded.", "Risk": "Short or missing password requirements weaken authentication, enabling **brute-force**, **password spraying**, and faster **offline cracking**. Compromised accounts can enable unauthorized console/API use, leading to **data exfiltration** (C), **unauthorized changes** (I), and service disruption via destructive actions (A).", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse.metadata.json b/prowler/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse.metadata.json index 6ecd834aa7..15d06140ec 100644 --- a/prowler/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse.metadata.json +++ b/prowler/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "User", "ResourceGroup": "IAM", - "Description": "**OCI Identity Domains** password policies are evaluated for **password reuse prevention** via the **password history** setting. The finding expects a configured policy with `num_passwords_in_history` set to at least `24`. *Legacy IAM password policies lack password history; only Identity Domains support this setting.*", + "Description": "**OCI Identity Domains** password policies are evaluated for **password reuse prevention** via **password history** (`num_passwords_in_history >= 24`). System-managed policies (`SimplePasswordPolicy`, `StandardPasswordPolicy`) are excluded. *Legacy IAM lacks password history.*", "Risk": "Without **password history**, users can reuse old passwords. Compromised credentials remain valid after resets, enabling account takeover, unauthorized changes, and **lateral movement**, degrading **confidentiality** and **integrity** and weakening recovery from password-related incidents.", "RelatedUrl": "", "AdditionalURLs": [ diff --git a/prowler/providers/oraclecloud/services/identity/identity_service.py b/prowler/providers/oraclecloud/services/identity/identity_service.py index b6d488966c..12b3c7e7e9 100644 --- a/prowler/providers/oraclecloud/services/identity/identity_service.py +++ b/prowler/providers/oraclecloud/services/identity/identity_service.py @@ -518,6 +518,14 @@ class Identity(OCIService): policies_response = domain_client.list_password_policies() for policy in policies_response.data.resources: + # Skip system-managed immutable policies that are + # hidden in the OCI Console and not user-configurable + if policy.id in ( + "SimplePasswordPolicy", + "StandardPasswordPolicy", + ): + continue + domain.password_policies.append( DomainPasswordPolicy( id=policy.id, diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days_test.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days_test.py index 189aa115a5..8e1ea7719e 100644 --- a/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days_test.py +++ b/tests/providers/oraclecloud/services/identity/identity_password_policy_expires_within_365_days/identity_password_policy_expires_within_365_days_test.py @@ -1,5 +1,10 @@ +from datetime import datetime, timezone from unittest import mock +from prowler.providers.oraclecloud.services.identity.identity_service import ( + DomainPasswordPolicy, + IdentityDomain, +) from tests.providers.oraclecloud.oci_fixtures import ( OCI_COMPARTMENT_ID, OCI_REGION, @@ -7,36 +12,34 @@ from tests.providers.oraclecloud.oci_fixtures import ( set_mocked_oraclecloud_provider, ) +DOMAIN_ID = "ocid1.domain.oc1..aaaaaaaexample" +DOMAIN_NAME = "Default" +DOMAIN_URL = "https://idcs-example.identity.oraclecloud.com" +POLICY_ID = "ocid1.passwordpolicy.oc1..aaaaaaaexample" +POLICY_NAME = "CustomPasswordPolicy" + + +def _make_domain(password_policies=None): + return IdentityDomain( + id=DOMAIN_ID, + display_name=DOMAIN_NAME, + description="Default identity domain", + url=DOMAIN_URL, + home_region=OCI_REGION, + compartment_id=OCI_COMPARTMENT_ID, + lifecycle_state="ACTIVE", + time_created=datetime.now(timezone.utc), + region=OCI_REGION, + password_policies=password_policies or [], + ) + class Test_identity_password_policy_expires_within_365_days: - def test_no_resources(self): - """identity_password_policy_expires_within_365_days: No resources to check""" + def test_no_domains(self): + """No Identity Domains → MANUAL finding.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock empty collections - identity_client.rules = [] - identity_client.topics = [] - identity_client.subscriptions = [] - identity_client.users = [] - identity_client.groups = [] - identity_client.policies = [] - identity_client.compartments = [] - identity_client.instances = [] - identity_client.volumes = [] - identity_client.boot_volumes = [] - identity_client.buckets = [] - identity_client.keys = [] - identity_client.file_systems = [] - identity_client.databases = [] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.subnets = [] - identity_client.vcns = [] - identity_client.configuration = None - identity_client.active_non_root_compartments = [] - identity_client.password_policy = None + identity_client.domains = [] with ( mock.patch( @@ -55,134 +58,29 @@ class Test_identity_password_policy_expires_within_365_days: check = identity_password_policy_expires_within_365_days() result = check.execute() - # Verify result is a list (empty or with findings) - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "MANUAL" - def test_resource_compliant(self): - """identity_password_policy_expires_within_365_days: Resource passes the check (PASS)""" + def test_policy_expires_within_365_days(self): + """Password expires within 365 days → PASS.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.aaaaaaaexample" - resource.name = "compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Production"} - - # Set attributes that make the resource compliant - resource.versioning = "Enabled" - resource.is_auto_rotation_enabled = True - resource.rotation_interval_in_days = 90 - resource.public_access_type = "NoPublicAccess" - resource.logging_enabled = True - resource.kms_key_id = "ocid1.key.oc1.iad.aaaaaaaexample" - resource.in_transit_encryption = "ENABLED" - resource.is_secure_boot_enabled = True - resource.legacy_endpoint_disabled = True - resource.is_legacy_imds_endpoint_disabled = True - - # Mock client with compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_oraclecloud_provider(), - ), - mock.patch( - "prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days.identity_client", - new=identity_client, - ), - ): - from prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days import ( - identity_password_policy_expires_within_365_days, - ) - - check = identity_password_policy_expires_within_365_days() - result = check.execute() - - assert isinstance(result, list) - - # If results exist, verify PASS findings - if len(result) > 0: - # Find PASS results - pass_results = [r for r in result if r.status == "PASS"] - - if pass_results: - # Detailed assertions on first PASS result - assert pass_results[0].status == "PASS" - assert pass_results[0].status_extended is not None - assert len(pass_results[0].status_extended) > 0 - - # Verify resource identification - assert pass_results[0].resource_id is not None - assert pass_results[0].resource_name is not None - assert pass_results[0].region is not None - assert pass_results[0].compartment_id is not None - - # Verify metadata - assert pass_results[0].check_metadata.Provider == "oraclecloud" - assert ( - pass_results[0].check_metadata.CheckID - == "identity_password_policy_expires_within_365_days" + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, ) - assert pass_results[0].check_metadata.ServiceName == "identity" - - def test_resource_non_compliant(self): - """identity_password_policy_expires_within_365_days: Resource fails the check (FAIL)""" - identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} - identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a non-compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.bbbbbbbexample" - resource.name = "non-compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Development"} - - # Set attributes that make the resource non-compliant - resource.versioning = "Disabled" - resource.is_auto_rotation_enabled = False - resource.rotation_interval_in_days = None - resource.public_access_type = "ObjectRead" - resource.logging_enabled = False - resource.kms_key_id = None - resource.in_transit_encryption = "DISABLED" - resource.is_secure_boot_enabled = False - resource.legacy_endpoint_disabled = False - resource.is_legacy_imds_endpoint_disabled = False - - # Mock client with non-compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] + ] + ) + ] with ( mock.patch( @@ -201,29 +99,139 @@ class Test_identity_password_policy_expires_within_365_days: check = identity_password_policy_expires_within_365_days() result = check.execute() - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "PASS" + assert "90 days" in result[0].status_extended + assert result[0].resource_id == POLICY_ID - # Verify FAIL findings exist - if len(result) > 0: - # Find FAIL results - fail_results = [r for r in result if r.status == "FAIL"] - - if fail_results: - # Detailed assertions on first FAIL result - assert fail_results[0].status == "FAIL" - assert fail_results[0].status_extended is not None - assert len(fail_results[0].status_extended) > 0 - - # Verify resource identification - assert fail_results[0].resource_id is not None - assert fail_results[0].resource_name is not None - assert fail_results[0].region is not None - assert fail_results[0].compartment_id is not None - - # Verify metadata - assert fail_results[0].check_metadata.Provider == "oraclecloud" - assert ( - fail_results[0].check_metadata.CheckID - == "identity_password_policy_expires_within_365_days" + def test_policy_expires_over_365_days(self): + """Password expires after more than 365 days → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=500, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, ) - assert fail_results[0].check_metadata.ServiceName == "identity" + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days import ( + identity_password_policy_expires_within_365_days, + ) + + check = identity_password_policy_expires_within_365_days() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "500 days" in result[0].status_extended + + def test_policy_no_expiration_configured(self): + """No password expiration configured → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=None, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days import ( + identity_password_policy_expires_within_365_days, + ) + + check = identity_password_policy_expires_within_365_days() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "does not have password expiration" in result[0].status_extended + + def test_system_managed_policies_excluded(self): + """System-managed policies should not appear in domain.password_policies. + + This is a regression test: SimplePasswordPolicy and StandardPasswordPolicy + are filtered at the service layer, so checks never see them. + """ + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + # Only user-configurable policy in the domain (system ones filtered by service) + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_expires_within_365_days.identity_password_policy_expires_within_365_days import ( + identity_password_policy_expires_within_365_days, + ) + + check = identity_password_policy_expires_within_365_days() + result = check.execute() + + # Only 1 finding for the custom policy, none for system-managed + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == POLICY_ID diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14_test.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14_test.py index f624bb3f14..b0165f071c 100644 --- a/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14_test.py +++ b/tests/providers/oraclecloud/services/identity/identity_password_policy_minimum_length_14/identity_password_policy_minimum_length_14_test.py @@ -1,5 +1,11 @@ +from datetime import datetime, timezone from unittest import mock +from prowler.providers.oraclecloud.services.identity.identity_service import ( + DomainPasswordPolicy, + IdentityDomain, + PasswordPolicy, +) from tests.providers.oraclecloud.oci_fixtures import ( OCI_COMPARTMENT_ID, OCI_REGION, @@ -7,36 +13,36 @@ from tests.providers.oraclecloud.oci_fixtures import ( set_mocked_oraclecloud_provider, ) +DOMAIN_ID = "ocid1.domain.oc1..aaaaaaaexample" +DOMAIN_NAME = "Default" +DOMAIN_URL = "https://idcs-example.identity.oraclecloud.com" +POLICY_ID = "ocid1.passwordpolicy.oc1..aaaaaaaexample" +POLICY_NAME = "CustomPasswordPolicy" + + +def _make_domain(password_policies=None): + return IdentityDomain( + id=DOMAIN_ID, + display_name=DOMAIN_NAME, + description="Default identity domain", + url=DOMAIN_URL, + home_region=OCI_REGION, + compartment_id=OCI_COMPARTMENT_ID, + lifecycle_state="ACTIVE", + time_created=datetime.now(timezone.utc), + region=OCI_REGION, + password_policies=password_policies or [], + ) + class Test_identity_password_policy_minimum_length_14: - def test_no_resources(self): - """identity_password_policy_minimum_length_14: No resources to check""" + def test_no_domains_no_legacy_policy(self): + """No domains and no legacy policy → FAIL.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock empty collections - identity_client.rules = [] - identity_client.topics = [] - identity_client.subscriptions = [] - identity_client.users = [] - identity_client.groups = [] - identity_client.policies = [] - identity_client.compartments = [] - identity_client.instances = [] - identity_client.volumes = [] - identity_client.boot_volumes = [] - identity_client.buckets = [] - identity_client.keys = [] - identity_client.file_systems = [] - identity_client.databases = [] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.subnets = [] - identity_client.vcns = [] - identity_client.configuration = None - identity_client.active_non_root_compartments = [] + identity_client.domains = [] identity_client.password_policy = None + identity_client.provider.identity.region = OCI_REGION with ( mock.patch( @@ -55,49 +61,30 @@ class Test_identity_password_policy_minimum_length_14: check = identity_password_policy_minimum_length_14() result = check.execute() - # Verify result is a list (empty or with findings) - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "No password policy" in result[0].status_extended - def test_resource_compliant(self): - """identity_password_policy_minimum_length_14: Resource passes the check (PASS)""" + def test_domain_policy_min_length_14(self): + """Domain password policy with min_length >= 14 → PASS.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.aaaaaaaexample" - resource.name = "compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Production"} - - # Set attributes that make the resource compliant - resource.versioning = "Enabled" - resource.is_auto_rotation_enabled = True - resource.rotation_interval_in_days = 90 - resource.public_access_type = "NoPublicAccess" - resource.logging_enabled = True - resource.kms_key_id = "ocid1.key.oc1.iad.aaaaaaaexample" - resource.in_transit_encryption = "ENABLED" - resource.is_secure_boot_enabled = True - resource.legacy_endpoint_disabled = True - resource.is_legacy_imds_endpoint_disabled = True - - # Mock client with compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] with ( mock.patch( @@ -116,73 +103,31 @@ class Test_identity_password_policy_minimum_length_14: check = identity_password_policy_minimum_length_14() result = check.execute() - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "PASS" + assert "14 characters" in result[0].status_extended + assert result[0].resource_id == POLICY_ID - # If results exist, verify PASS findings - if len(result) > 0: - # Find PASS results - pass_results = [r for r in result if r.status == "PASS"] - - if pass_results: - # Detailed assertions on first PASS result - assert pass_results[0].status == "PASS" - assert pass_results[0].status_extended is not None - assert len(pass_results[0].status_extended) > 0 - - # Verify resource identification - assert pass_results[0].resource_id is not None - assert pass_results[0].resource_name is not None - assert pass_results[0].region is not None - assert pass_results[0].compartment_id is not None - - # Verify metadata - assert pass_results[0].check_metadata.Provider == "oraclecloud" - assert ( - pass_results[0].check_metadata.CheckID - == "identity_password_policy_minimum_length_14" - ) - assert pass_results[0].check_metadata.ServiceName == "identity" - - def test_resource_non_compliant(self): - """identity_password_policy_minimum_length_14: Resource fails the check (FAIL)""" + def test_domain_policy_min_length_too_short(self): + """Domain password policy with min_length < 14 → FAIL.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a non-compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.bbbbbbbexample" - resource.name = "non-compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Development"} - - # Set attributes that make the resource non-compliant - resource.versioning = "Disabled" - resource.is_auto_rotation_enabled = False - resource.rotation_interval_in_days = None - resource.public_access_type = "ObjectRead" - resource.logging_enabled = False - resource.kms_key_id = None - resource.in_transit_encryption = "DISABLED" - resource.is_secure_boot_enabled = False - resource.legacy_endpoint_disabled = False - resource.is_legacy_imds_endpoint_disabled = False - - # Mock client with non-compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=8, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] with ( mock.patch( @@ -201,29 +146,116 @@ class Test_identity_password_policy_minimum_length_14: check = identity_password_policy_minimum_length_14() result = check.execute() - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "8" in result[0].status_extended - # Verify FAIL findings exist - if len(result) > 0: - # Find FAIL results - fail_results = [r for r in result if r.status == "FAIL"] + def test_legacy_policy_compliant(self): + """Legacy password policy with min length >= 14 → PASS.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [] + identity_client.password_policy = PasswordPolicy( + is_lowercase_characters_required=True, + is_uppercase_characters_required=True, + is_numeric_characters_required=True, + is_special_characters_required=True, + is_username_containment_allowed=False, + minimum_password_length=14, + ) + identity_client.provider.identity.region = OCI_REGION - if fail_results: - # Detailed assertions on first FAIL result - assert fail_results[0].status == "FAIL" - assert fail_results[0].status_extended is not None - assert len(fail_results[0].status_extended) > 0 + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14 import ( + identity_password_policy_minimum_length_14, + ) - # Verify resource identification - assert fail_results[0].resource_id is not None - assert fail_results[0].resource_name is not None - assert fail_results[0].region is not None - assert fail_results[0].compartment_id is not None + check = identity_password_policy_minimum_length_14() + result = check.execute() - # Verify metadata - assert fail_results[0].check_metadata.Provider == "oraclecloud" - assert ( - fail_results[0].check_metadata.CheckID - == "identity_password_policy_minimum_length_14" + assert len(result) == 1 + assert result[0].status == "PASS" + assert "14 characters" in result[0].status_extended + + def test_domain_no_policies(self): + """Domain with no password policies → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [_make_domain([])] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14 import ( + identity_password_policy_minimum_length_14, + ) + + check = identity_password_policy_minimum_length_14() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "no password policy configured" in result[0].status_extended + + def test_system_managed_policies_excluded(self): + """System-managed policies should not appear in domain.password_policies. + + This is a regression test: SimplePasswordPolicy and StandardPasswordPolicy + are filtered at the service layer, so checks never see them. + """ + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, ) - assert fail_results[0].check_metadata.ServiceName == "identity" + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_minimum_length_14.identity_password_policy_minimum_length_14 import ( + identity_password_policy_minimum_length_14, + ) + + check = identity_password_policy_minimum_length_14() + result = check.execute() + + # Only 1 finding for the custom policy, none for system-managed + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == POLICY_ID diff --git a/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse_test.py b/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse_test.py index 07a168ab4c..4754f9c0c4 100644 --- a/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse_test.py +++ b/tests/providers/oraclecloud/services/identity/identity_password_policy_prevents_reuse/identity_password_policy_prevents_reuse_test.py @@ -1,5 +1,10 @@ +from datetime import datetime, timezone from unittest import mock +from prowler.providers.oraclecloud.services.identity.identity_service import ( + DomainPasswordPolicy, + IdentityDomain, +) from tests.providers.oraclecloud.oci_fixtures import ( OCI_COMPARTMENT_ID, OCI_REGION, @@ -7,36 +12,34 @@ from tests.providers.oraclecloud.oci_fixtures import ( set_mocked_oraclecloud_provider, ) +DOMAIN_ID = "ocid1.domain.oc1..aaaaaaaexample" +DOMAIN_NAME = "Default" +DOMAIN_URL = "https://idcs-example.identity.oraclecloud.com" +POLICY_ID = "ocid1.passwordpolicy.oc1..aaaaaaaexample" +POLICY_NAME = "CustomPasswordPolicy" + + +def _make_domain(password_policies=None): + return IdentityDomain( + id=DOMAIN_ID, + display_name=DOMAIN_NAME, + description="Default identity domain", + url=DOMAIN_URL, + home_region=OCI_REGION, + compartment_id=OCI_COMPARTMENT_ID, + lifecycle_state="ACTIVE", + time_created=datetime.now(timezone.utc), + region=OCI_REGION, + password_policies=password_policies or [], + ) + class Test_identity_password_policy_prevents_reuse: - def test_no_resources(self): - """identity_password_policy_prevents_reuse: No resources to check""" + def test_no_domains(self): + """No Identity Domains → MANUAL finding.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock empty collections - identity_client.rules = [] - identity_client.topics = [] - identity_client.subscriptions = [] - identity_client.users = [] - identity_client.groups = [] - identity_client.policies = [] - identity_client.compartments = [] - identity_client.instances = [] - identity_client.volumes = [] - identity_client.boot_volumes = [] - identity_client.buckets = [] - identity_client.keys = [] - identity_client.file_systems = [] - identity_client.databases = [] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.subnets = [] - identity_client.vcns = [] - identity_client.configuration = None - identity_client.active_non_root_compartments = [] - identity_client.password_policy = None + identity_client.domains = [] with ( mock.patch( @@ -55,134 +58,29 @@ class Test_identity_password_policy_prevents_reuse: check = identity_password_policy_prevents_reuse() result = check.execute() - # Verify result is a list (empty or with findings) - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "MANUAL" - def test_resource_compliant(self): - """identity_password_policy_prevents_reuse: Resource passes the check (PASS)""" + def test_policy_prevents_reuse_24(self): + """Password history >= 24 → PASS.""" identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.aaaaaaaexample" - resource.name = "compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Production"} - - # Set attributes that make the resource compliant - resource.versioning = "Enabled" - resource.is_auto_rotation_enabled = True - resource.rotation_interval_in_days = 90 - resource.public_access_type = "NoPublicAccess" - resource.logging_enabled = True - resource.kms_key_id = "ocid1.key.oc1.iad.aaaaaaaexample" - resource.in_transit_encryption = "ENABLED" - resource.is_secure_boot_enabled = True - resource.legacy_endpoint_disabled = True - resource.is_legacy_imds_endpoint_disabled = True - - # Mock client with compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_oraclecloud_provider(), - ), - mock.patch( - "prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse.identity_client", - new=identity_client, - ), - ): - from prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse import ( - identity_password_policy_prevents_reuse, - ) - - check = identity_password_policy_prevents_reuse() - result = check.execute() - - assert isinstance(result, list) - - # If results exist, verify PASS findings - if len(result) > 0: - # Find PASS results - pass_results = [r for r in result if r.status == "PASS"] - - if pass_results: - # Detailed assertions on first PASS result - assert pass_results[0].status == "PASS" - assert pass_results[0].status_extended is not None - assert len(pass_results[0].status_extended) > 0 - - # Verify resource identification - assert pass_results[0].resource_id is not None - assert pass_results[0].resource_name is not None - assert pass_results[0].region is not None - assert pass_results[0].compartment_id is not None - - # Verify metadata - assert pass_results[0].check_metadata.Provider == "oraclecloud" - assert ( - pass_results[0].check_metadata.CheckID - == "identity_password_policy_prevents_reuse" + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, ) - assert pass_results[0].check_metadata.ServiceName == "identity" - - def test_resource_non_compliant(self): - """identity_password_policy_prevents_reuse: Resource fails the check (FAIL)""" - identity_client = mock.MagicMock() - identity_client.audited_compartments = {OCI_COMPARTMENT_ID: mock.MagicMock()} - identity_client.audited_tenancy = OCI_TENANCY_ID - - # Mock a non-compliant resource - resource = mock.MagicMock() - resource.id = "ocid1.resource.oc1.iad.bbbbbbbexample" - resource.name = "non-compliant-resource" - resource.region = OCI_REGION - resource.compartment_id = OCI_COMPARTMENT_ID - resource.lifecycle_state = "ACTIVE" - resource.tags = {"Environment": "Development"} - - # Set attributes that make the resource non-compliant - resource.versioning = "Disabled" - resource.is_auto_rotation_enabled = False - resource.rotation_interval_in_days = None - resource.public_access_type = "ObjectRead" - resource.logging_enabled = False - resource.kms_key_id = None - resource.in_transit_encryption = "DISABLED" - resource.is_secure_boot_enabled = False - resource.legacy_endpoint_disabled = False - resource.is_legacy_imds_endpoint_disabled = False - - # Mock client with non-compliant resource - identity_client.buckets = [resource] - identity_client.keys = [resource] - identity_client.volumes = [resource] - identity_client.boot_volumes = [resource] - identity_client.instances = [resource] - identity_client.file_systems = [resource] - identity_client.databases = [resource] - identity_client.security_lists = [] - identity_client.security_groups = [] - identity_client.rules = [] - identity_client.configuration = resource - identity_client.users = [] + ] + ) + ] with ( mock.patch( @@ -201,29 +99,165 @@ class Test_identity_password_policy_prevents_reuse: check = identity_password_policy_prevents_reuse() result = check.execute() - assert isinstance(result, list) + assert len(result) == 1 + assert result[0].status == "PASS" + assert "24 passwords" in result[0].status_extended + assert result[0].resource_id == POLICY_ID - # Verify FAIL findings exist - if len(result) > 0: - # Find FAIL results - fail_results = [r for r in result if r.status == "FAIL"] - - if fail_results: - # Detailed assertions on first FAIL result - assert fail_results[0].status == "FAIL" - assert fail_results[0].status_extended is not None - assert len(fail_results[0].status_extended) > 0 - - # Verify resource identification - assert fail_results[0].resource_id is not None - assert fail_results[0].resource_name is not None - assert fail_results[0].region is not None - assert fail_results[0].compartment_id is not None - - # Verify metadata - assert fail_results[0].check_metadata.Provider == "oraclecloud" - assert ( - fail_results[0].check_metadata.CheckID - == "identity_password_policy_prevents_reuse" + def test_policy_insufficient_history(self): + """Password history < 24 → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=5, + password_expire_warning=7, + min_password_age=1, ) - assert fail_results[0].check_metadata.ServiceName == "identity" + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse import ( + identity_password_policy_prevents_reuse, + ) + + check = identity_password_policy_prevents_reuse() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "5 passwords" in result[0].status_extended + + def test_policy_no_history_configured(self): + """No password history configured → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=None, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse import ( + identity_password_policy_prevents_reuse, + ) + + check = identity_password_policy_prevents_reuse() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "does not have password history" in result[0].status_extended + + def test_domain_no_policies(self): + """Domain with no password policies → FAIL.""" + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [_make_domain([])] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse import ( + identity_password_policy_prevents_reuse, + ) + + check = identity_password_policy_prevents_reuse() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "no password policy configured" in result[0].status_extended + + def test_system_managed_policies_excluded(self): + """System-managed policies should not appear in domain.password_policies. + + This is a regression test: SimplePasswordPolicy and StandardPasswordPolicy + are filtered at the service layer, so checks never see them. + """ + identity_client = mock.MagicMock() + identity_client.audited_tenancy = OCI_TENANCY_ID + identity_client.domains = [ + _make_domain( + [ + DomainPasswordPolicy( + id=POLICY_ID, + name=POLICY_NAME, + description="Custom policy", + min_length=14, + password_expires_after=90, + num_passwords_in_history=24, + password_expire_warning=7, + min_password_age=1, + ) + ] + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_oraclecloud_provider(), + ), + mock.patch( + "prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse.identity_client", + new=identity_client, + ), + ): + from prowler.providers.oraclecloud.services.identity.identity_password_policy_prevents_reuse.identity_password_policy_prevents_reuse import ( + identity_password_policy_prevents_reuse, + ) + + check = identity_password_policy_prevents_reuse() + result = check.execute() + + # Only 1 finding for the custom policy, none for system-managed + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == POLICY_ID