From 0e9de48a260ef42e3566bf8e71dcc90b2434dca0 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Tue, 4 Mar 2025 13:23:07 +0100 Subject: [PATCH 1/2] feat(entra): add new check for admin consent workflow --- .../__init__.py | 0 ...min_consent_workflow_enabled.metadata.json | 30 ++++ .../entra_admin_consent_workflow_enabled.py | 52 ++++++ .../services/entra/entra_service.py | 29 ++- ...tra_admin_consent_workflow_enabled_test.py | 169 ++++++++++++++++++ .../entra/microsoft365_entra_service_test.py | 21 +++ 6 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/__init__.py create mode 100644 prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json create mode 100644 prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.py create mode 100644 tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/__init__.py b/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json b/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json new file mode 100644 index 0000000000..a70b98501a --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_admin_consent_workflow_enabled", + "CheckTitle": "Ensure the admin consent workflow is enabled.", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Organization Settings", + "Description": "Ensure that the admin consent workflow is enabled in Microsoft Entra to allow users to request admin approval for applications requiring consent.", + "Risk": "If the admin consent workflow is not enabled, users may be blocked from accessing applications that require admin consent, leading to potential work disruptions or unauthorized workarounds.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity > Applications and select Enterprise applications. 3. Under Security, select Consent and permissions. 4. Under Manage, select Admin consent settings. 5. Set 'Users can request admin consent to apps they are unable to consent to' to 'Yes'. 6. Configure the reviewers and email notifications settings. 7. Click Save.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the admin consent workflow in Microsoft Entra to securely manage application consent requests.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.py b/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.py new file mode 100644 index 0000000000..24bcbe9c47 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled.py @@ -0,0 +1,52 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportMicrosoft365 +from prowler.providers.microsoft365.services.entra.entra_client import entra_client + + +class entra_admin_consent_workflow_enabled(Check): + """ + Ensure the admin consent workflow is enabled in Microsoft Entra. + + This check verifies that the admin consent workflow is enabled in Microsoft Entra to allow users + to request admin approval for applications requiring consent. Enabling the admin consent workflow + ensures that applications which require additional permissions are only granted access after an + administrator’s approval, reducing the risk of unauthorized access and work disruptions. + + The check fails if the admin consent workflow is not enabled, indicating that users might be blocked + from accessing critical applications or forced to use insecure workarounds. + """ + + def execute(self) -> List[CheckReportMicrosoft365]: + """ + Execute the admin consent workflow requirement check. + + Retrieves the admin consent policy from the Microsoft Entra client and generates a report indicating + whether the admin consent workflow is enabled. + + Returns: + List[CheckReportMicrosoft365]: A list containing the report with the result of the check. + """ + findings = [] + admin_consent_policy = entra_client.admin_consent_policy + if admin_consent_policy: + report = CheckReportMicrosoft365( + self.metadata(), + resource=admin_consent_policy, + resource_name="Admin Consent Policy", + resource_id=entra_client.tenant_domain, + ) + report.status = "FAIL" + report.status_extended = "The admin consent workflow is not enabled in Microsoft Entra; users may be blocked from accessing applications that require admin consent." + if admin_consent_policy.admin_consent_enabled: + report.status = "PASS" + report.status_extended = "The admin consent workflow is enabled in Microsoft Entra, allowing users to request admin approval for applications." + if admin_consent_policy.notify_reviewers: + report.status_extended += " Reviewers will be notified." + else: + report.status_extended += ( + " Reviewers will not be notified, we recommend notifying them." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index 14d04ad93a..f4fff30656 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -13,14 +13,16 @@ def __init__(self, provider: Microsoft365Provider): super().__init__(provider) loop = get_event_loop() - + self.tenant_domain = provider.identity.tenant_domain attributes = loop.run_until_complete( gather( self._get_authorization_policy(), + self._get_admin_consent_poolicy(), ) ) self.authorization_policy = attributes[0] + self.admin_consent_poolicy = attributes[1] async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") @@ -83,6 +85,24 @@ async def _get_authorization_policy(self): return authorization_policy + async def _get_admin_consent_poolicy(self): + logger.info("Entra - Getting group settings...") + admin_consent_policy = None + try: + policy = await self.client.policies.admin_consent_request_policy.get() + admin_consent_policy = AdminConsentPolicy( + admin_consent_enabled=policy.is_enabled, + notify_reviewers=policy.notify_reviewers, + email_reminders_to_reviewers=policy.reminders_enabled, + duration_in_days=policy.request_duration_in_days, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return admin_consent_policy + class DefaultUserRolePermissions(BaseModel): allowed_to_create_apps: Optional[bool] @@ -99,3 +119,10 @@ class AuthorizationPolicy(BaseModel): name: str description: str default_user_role_permissions: Optional[DefaultUserRolePermissions] + + +class AdminConsentPolicy(BaseModel): + admin_consent_enabled: bool + notify_reviewers: bool + email_reminders_to_reviewers: bool + duration_in_days: int diff --git a/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py new file mode 100644 index 0000000000..dddf859c7f --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py @@ -0,0 +1,169 @@ +from unittest import mock + +from prowler.providers.microsoft365.services.entra.entra_service import ( + AdminConsentPolicy, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_entra_admin_consent_workflow_enabled: + def test_admin_consent_enabled(self): + """ + Test when admin_consent_enabled is True: + The check should PASS because the admin consent workflow is enabled. + """ + entra_client = mock.MagicMock() + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled import ( + entra_admin_consent_workflow_enabled, + ) + + entra_client.admin_consent_policy = AdminConsentPolicy( + admin_consent_enabled=True, + notify_reviewers=True, + email_reminders_to_reviewers=False, + duration_in_days=30, + ) + entra_client.tenant_domain = DOMAIN + + check = entra_admin_consent_workflow_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "The admin consent workflow is enabled in Microsoft Entra, allowing users to request admin approval for applications. Reviewers will be notified." + ) + assert result[0].resource_id == DOMAIN + assert result[0].location == "global" + assert result[0].resource_name == "Admin Consent Policy" + assert result[0].resource == { + "admin_consent_enabled": True, + "notify_reviewers": True, + "email_reminders_to_reviewers": False, + "duration_in_days": 30, + } + + def test_admin_consent_enabled_without_notifications(self): + """ + Test when admin_consent_enabled is True: + The check should PASS because the admin consent workflow is enabled. + """ + entra_client = mock.MagicMock() + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled import ( + entra_admin_consent_workflow_enabled, + ) + + entra_client.admin_consent_policy = AdminConsentPolicy( + admin_consent_enabled=True, + notify_reviewers=False, + email_reminders_to_reviewers=False, + duration_in_days=30, + ) + entra_client.tenant_domain = DOMAIN + + check = entra_admin_consent_workflow_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "The admin consent workflow is enabled in Microsoft Entra, allowing users to request admin approval for applications. Reviewers will not be notified, we recommend notifying them." + ) + assert result[0].resource_id == DOMAIN + assert result[0].location == "global" + assert result[0].resource_name == "Admin Consent Policy" + assert result[0].resource == { + "admin_consent_enabled": True, + "notify_reviewers": False, + "email_reminders_to_reviewers": False, + "duration_in_days": 30, + } + + def test_admin_consent_disabled(self): + """ + Test when admin_consent_enabled is False: + The check should FAIL because the admin consent workflow is not enabled. + """ + entra_client = mock.MagicMock() + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled import ( + entra_admin_consent_workflow_enabled, + ) + + entra_client.admin_consent_policy = AdminConsentPolicy( + admin_consent_enabled=False, + notify_reviewers=True, + email_reminders_to_reviewers=False, + duration_in_days=30, + ) + entra_client.tenant_domain = DOMAIN + + check = entra_admin_consent_workflow_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "The admin consent workflow is not enabled in Microsoft Entra; users may be blocked from accessing applications that require admin consent." + ) + assert result[0].resource_id == DOMAIN + assert result[0].location == "global" + assert result[0].resource_name == "Admin Consent Policy" + assert result[0].resource == { + "admin_consent_enabled": False, + "notify_reviewers": True, + "email_reminders_to_reviewers": False, + "duration_in_days": 30, + } + + def test_no_policy(self): + """ + Test when entra_client.admin_consent_poolicy is None: + The check should return an empty list of findings. + """ + entra_client = mock.MagicMock() + entra_client.admin_consent_policy = None + entra_client.tenant_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.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled.entra_client", + new=entra_client, + ): + from prowler.providers.microsoft365.services.entra.entra_admin_consent_workflow_enabled.entra_admin_consent_workflow_enabled import ( + entra_admin_consent_workflow_enabled, + ) + + check = entra_admin_consent_workflow_enabled() + result = check.execute() + + assert len(result) == 0 + assert result == [] diff --git a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py index caf9fdec74..944b9455e2 100644 --- a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -2,6 +2,7 @@ from prowler.providers.microsoft365.models import Microsoft365IdentityInfo from prowler.providers.microsoft365.services.entra.entra_service import ( + AdminConsentPolicy, AuthorizationPolicy, DefaultUserRolePermissions, Entra, @@ -27,6 +28,15 @@ async def mock_entra_get_authorization_policy(_): ) +async def mock_entra_get_admin_consent_poolicy(_): + return AdminConsentPolicy( + admin_consent_enabled=True, + notify_reviewers=True, + email_reminders_to_reviewers=False, + duration_in_days=30, + ) + + class Test_Entra_Service: def test_get_client(self): admincenter_client = Entra( @@ -55,3 +65,14 @@ def test_get_authorization_policy(self): allowed_to_read_other_users=True, ) ) + + @patch( + "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_admin_consent_poolicy", + new=mock_entra_get_admin_consent_poolicy, + ) + def test_get_admin_consent_poolicy(self): + entra_client = Entra(set_mocked_microsoft365_provider()) + assert entra_client.admin_consent_poolicy.admin_consent_enabled + assert entra_client.admin_consent_poolicy.notify_reviewers + assert entra_client.admin_consent_poolicy.email_reminders_to_reviewers is False + assert entra_client.admin_consent_poolicy.duration_in_days == 30 From 3f592997a648cd763718b5136611abde37213fc0 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Tue, 4 Mar 2025 18:03:59 +0100 Subject: [PATCH 2/2] fix: typo --- .../microsoft365/services/entra/entra_service.py | 6 +++--- .../entra_admin_consent_workflow_enabled_test.py | 2 +- .../entra/microsoft365_entra_service_test.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index f4fff30656..327675aacc 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -17,12 +17,12 @@ def __init__(self, provider: Microsoft365Provider): attributes = loop.run_until_complete( gather( self._get_authorization_policy(), - self._get_admin_consent_poolicy(), + self._get_admin_consent_policy(), ) ) self.authorization_policy = attributes[0] - self.admin_consent_poolicy = attributes[1] + self.admin_consent_policy = attributes[1] async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") @@ -85,7 +85,7 @@ async def _get_authorization_policy(self): return authorization_policy - async def _get_admin_consent_poolicy(self): + async def _get_admin_consent_policy(self): logger.info("Entra - Getting group settings...") admin_consent_policy = None try: diff --git a/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py index dddf859c7f..7394589465 100644 --- a/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py +++ b/tests/providers/microsoft365/services/entra/entra_admin_consent_workflow_enabled/entra_admin_consent_workflow_enabled_test.py @@ -144,7 +144,7 @@ def test_admin_consent_disabled(self): def test_no_policy(self): """ - Test when entra_client.admin_consent_poolicy is None: + Test when entra_client.admin_consent_policy is None: The check should return an empty list of findings. """ entra_client = mock.MagicMock() diff --git a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py index 944b9455e2..9fa1ceb958 100644 --- a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -28,7 +28,7 @@ async def mock_entra_get_authorization_policy(_): ) -async def mock_entra_get_admin_consent_poolicy(_): +async def mock_entra_get_admin_consent_policy(_): return AdminConsentPolicy( admin_consent_enabled=True, notify_reviewers=True, @@ -67,12 +67,12 @@ def test_get_authorization_policy(self): ) @patch( - "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_admin_consent_poolicy", - new=mock_entra_get_admin_consent_poolicy, + "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_admin_consent_policy", + new=mock_entra_get_admin_consent_policy, ) - def test_get_admin_consent_poolicy(self): + def test_get_admin_consent_policy(self): entra_client = Entra(set_mocked_microsoft365_provider()) - assert entra_client.admin_consent_poolicy.admin_consent_enabled - assert entra_client.admin_consent_poolicy.notify_reviewers - assert entra_client.admin_consent_poolicy.email_reminders_to_reviewers is False - assert entra_client.admin_consent_poolicy.duration_in_days == 30 + assert entra_client.admin_consent_policy.admin_consent_enabled + assert entra_client.admin_consent_policy.notify_reviewers + assert entra_client.admin_consent_policy.email_reminders_to_reviewers is False + assert entra_client.admin_consent_policy.duration_in_days == 30