From 43927a62f3ceb9e879da1eaf2f45a036fc9b3895 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Tue, 4 Feb 2025 23:24:11 +0100 Subject: [PATCH] feat(microsoft365): add new check `admincenter_settings_password_never_expire` (#6023) Co-authored-by: MrCloudSec --- .../{microsoft365_regions.py => regions.py} | 0 .../microsoft365/microsoft365_provider.py | 4 +- .../admincenter/admincenter_service.py | 29 +++++ .../__init__.py | 0 ...ttings_password_never_expire.metadata.json | 30 +++++ ...incenter_settings_password_never_expire.py | 27 +++++ ...sers_between_two_and_four_global_admins.py | 4 +- .../lib/regions/microsoft365_regions_test.py | 2 +- .../microsoft365/microsoft365_fixtures.py | 1 - .../microsoft365_provider_test.py | 2 - ...ter_settings_password_never_expire_test.py | 112 ++++++++++++++++++ 11 files changed, 201 insertions(+), 10 deletions(-) rename prowler/providers/microsoft365/lib/regions/{microsoft365_regions.py => regions.py} (100%) create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py diff --git a/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py b/prowler/providers/microsoft365/lib/regions/regions.py similarity index 100% rename from prowler/providers/microsoft365/lib/regions/microsoft365_regions.py rename to prowler/providers/microsoft365/lib/regions/regions.py diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index dff8da4d97..af7a723f67 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -50,9 +50,7 @@ from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( - get_regions_config, -) +from prowler.providers.microsoft365.lib.regions.regions import get_regions_config from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, Microsoft365RegionConfig, diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 2fc14d1ae6..fed450ca82 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -22,11 +22,13 @@ class AdminCenter(Microsoft365Service): gather( self._get_directory_roles(), self._get_groups(), + self._get_domains(), ) ) self.directory_roles = attributes[0] self.groups = attributes[1] + self.domains = attributes[2] async def _get_users(self): logger.info("Microsoft365 - Getting users...") @@ -130,6 +132,28 @@ class AdminCenter(Microsoft365Service): ) return groups + async def _get_domains(self): + logger.info("Microsoft365 - Getting domains...") + domains = {} + try: + domains_list = await self.client.domains.get() + domains.update({}) + for domain in domains_list.value: + domains.update( + { + domain.id: Domain( + id=domain.id, + password_validity_period=domain.password_validity_period_in_days, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return domains + class User(BaseModel): id: str @@ -149,3 +173,8 @@ class Group(BaseModel): id: str name: str visibility: str + + +class Domain(BaseModel): + id: str + password_validity_period: int diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json new file mode 100644 index 0000000000..13528b0008 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft", + "CheckID": "admincenter_settings_password_never_expire", + "CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Microsoft365Domain", + "Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.", + "Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.", + "RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide", + "Remediation": { + "Code": { + "CLI": "Set-MsolUser -UserPrincipalName -PasswordNeverExpires $true", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py new file mode 100644 index 0000000000..8de62dd8d5 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_settings_password_never_expire(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + for domain in admincenter_client.domains.values(): + report = Check_Report_Microsoft365(self.metadata(), resource=domain) + report.resource_id = domain.id + report.resource_name = domain.id + report.status = "FAIL" + report.status_extended = ( + f"Domain {domain.id} does not have a Password never expires policy." + ) + + if domain.password_validity_period == 2147483647: + report.status = "PASS" + report.status_extended = ( + f"Domain {domain.id} Password policy is set to never expire." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index aa0fc978c0..6002e67e4c 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -9,9 +9,7 @@ class admincenter_users_between_two_and_four_global_admins(Check): findings = [] directory_roles = admincenter_client.directory_roles - report = Check_Report_Microsoft365( - metadata=self.metadata(), resource=admincenter_client.directory_roles - ) + report = Check_Report_Microsoft365(metadata=self.metadata(), resource={}) report.status = "FAIL" report.resource_name = "Global Administrator" diff --git a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py index 3e358511b8..49ec5a480d 100644 --- a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py +++ b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py @@ -1,6 +1,6 @@ from azure.identity import AzureAuthorityHosts -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( +from prowler.providers.microsoft365.lib.regions.regions import ( MICROSOFT365_CHINA_CLOUD, MICROSOFT365_GENERIC_CLOUD, MICROSOFT365_US_GOV_CLOUD, diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py index 79e8ec118f..1dbb09c612 100644 --- a/tests/providers/microsoft365/microsoft365_fixtures.py +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -29,7 +29,6 @@ def set_mocked_microsoft365_provider( audit_config: dict = None, azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(), ) -> Microsoft365Provider: - provider = MagicMock() provider.type = "microsoft365" provider.session.credentials = credentials diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index b475a02ca6..7658301e44 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -212,7 +212,6 @@ class TestMicrosoft365Provider: "prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient" ) as mock_graph_client, ): - # Mock the return value of DefaultAzureCredential mock_credentials = MagicMock() mock_credentials.get_token.return_value = AccessToken( @@ -271,7 +270,6 @@ class TestMicrosoft365Provider: with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session: - mock_setup_session.side_effect = Microsoft365HTTPResponseError( file="test_file", original_exception="Simulated HttpResponseError" ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py new file mode 100644 index 0000000000..b49bc98095 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py @@ -0,0 +1,112 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_settings_password_never_expire: + def test_admincenter_no_domains(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + admincenter_client.domains = {} + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_domain_password_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=5), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Domain {id_domain} does not have a Password never expires policy." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain + + def test_admincenter_password_not_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=2147483647), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Domain {id_domain} Password policy is set to never expire." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain