mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-19 09:54:16 +00:00
feat(m365): add entra_policy_msol_powershell_blocked check (#13004)
Co-authored-by: kinomakino <3662588+kinomakino@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
This commit is contained in:
8 files changed
+291
No files matched your search
@@ -0,0 +1 @@
|
||||
`entra_policy_msol_powershell_blocked` check for M365 provider, verifying that the tenant authorization policy blocks the legacy MSOnline PowerShell module
|
||||
@@ -674,6 +674,7 @@
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
"entra_policy_msol_powershell_blocked",
|
||||
"entra_seamless_sso_disabled",
|
||||
"entra_service_principal_no_secrets_for_permanent_tier0_roles"
|
||||
]
|
||||
@@ -830,6 +831,7 @@
|
||||
],
|
||||
"Checks": [
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_msol_powershell_blocked",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_thirdparty_integrated_apps_not_allowed",
|
||||
"teams_external_domains_restricted",
|
||||
|
||||
Whitespace-only changes.
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_policy_msol_powershell_blocked",
|
||||
"CheckTitle": "Tenant blocks the legacy MSOnline (MSOL) PowerShell module",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "The tenant authorization policy property `blockMsolPowerShell` controls whether the retired **MSOnline (MSOL) PowerShell** module and its legacy service endpoint are accessible. When set to `true`, user-based access through that endpoint is disabled without affecting Microsoft Entra Connect or Microsoft Graph PowerShell.",
|
||||
"Risk": "The MSOnline and AzureAD PowerShell modules are **retired** and no longer receive security updates. They use a legacy endpoint that is less monitored than Microsoft Graph, providing an **unsupported administrative channel** to identity data and configuration. Attackers or insiders could exploit this path to read or modify directory objects while bypassing modern controls and audit coverage.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/graph/api/resources/authorizationpolicy?view=graph-rest-1.0",
|
||||
"https://learn.microsoft.com/powershell/azure/active-directory/overview",
|
||||
"https://learn.microsoft.com/powershell/microsoftgraph/migration-steps",
|
||||
"https://maester.dev/docs/tests/MT.1185"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "Update-MgPolicyAuthorizationPolicy -BodyParameter @{ blockMsolPowerShell = $true }",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Connect to Microsoft Graph PowerShell with the required scope:\n `Connect-MgGraph -Scopes \"Policy.ReadWrite.Authorization\"`\n2. Run:\n `Update-MgPolicyAuthorizationPolicy -BodyParameter @{ blockMsolPowerShell = $true }`\n3. Verify the change:\n `(Get-MgPolicyAuthorizationPolicy).BlockMsolPowerShell`\n\n*Note: There is no Entra admin center toggle for this setting. Before enabling, migrate any remaining scripts from MSOnline/AzureAD to Microsoft Graph PowerShell.*",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Block the legacy MSOnline PowerShell endpoint by setting `blockMsolPowerShell` to **true** in the authorization policy. Before enabling, migrate all automation and scripts from the retired MSOnline/AzureAD modules to **Microsoft Graph PowerShell**. This does not affect Microsoft Entra Connect sync or Microsoft Graph-based tooling.",
|
||||
"Url": "https://hub.prowler.com/check/entra_policy_msol_powershell_blocked"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "The blockMsolPowerShell setting is not enabled by default in every tenant and must be verified explicitly. It does not affect Microsoft Entra Connect or Microsoft Graph PowerShell."
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
"""Check if legacy MSOnline (MSOL) PowerShell module is blocked in the tenant."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_policy_msol_powershell_blocked(Check):
|
||||
"""Ensure legacy MSOnline (MSOL) PowerShell is blocked in the tenant authorization policy.
|
||||
|
||||
This check verifies the `block_msol_powershell` property of the authorization policy.
|
||||
The MSOnline and AzureAD PowerShell modules are retired and use a legacy endpoint
|
||||
that is less monitored than Microsoft Graph. Blocking them removes an unsupported
|
||||
administrative access channel.
|
||||
|
||||
- PASS: blockMsolPowerShell is true.
|
||||
- FAIL: blockMsolPowerShell is false.
|
||||
- MANUAL: The authorization policy could not be read or the property is missing/null.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""Execute the MSOL PowerShell blocked check.
|
||||
|
||||
Retrieves the authorization policy from the Microsoft Entra client and checks
|
||||
whether the 'block_msol_powershell' property is set to true.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list containing a single check report with
|
||||
the pass/fail/manual status and description.
|
||||
"""
|
||||
findings = []
|
||||
auth_policy = entra_client.authorization_policy
|
||||
|
||||
if auth_policy is None:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Authorization Policy",
|
||||
resource_id="authorizationPolicy",
|
||||
)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = "Cannot evaluate whether legacy MSOnline (MSOL) PowerShell is blocked: the authorization policy could not be retrieved. Check the scanning application's permissions and any Microsoft Graph API errors in the scan log."
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=auth_policy,
|
||||
resource_name=auth_policy.name,
|
||||
resource_id=auth_policy.id,
|
||||
)
|
||||
|
||||
block_msol = auth_policy.block_msol_powershell
|
||||
|
||||
if block_msol is None:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = "Legacy MSOnline (MSOL) PowerShell blocked status could not be determined from the tenant authorization policy."
|
||||
elif block_msol:
|
||||
report.status = "PASS"
|
||||
report.status_extended = "Legacy MSOnline (MSOL) PowerShell is blocked in the tenant authorization policy."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Legacy MSOnline (MSOL) PowerShell is not blocked in the tenant authorization policy."
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -227,6 +227,9 @@ class Entra(M365Service):
|
||||
),
|
||||
guest_invite_settings=auth_policy.allow_invites_from,
|
||||
guest_user_role_id=auth_policy.guest_user_role_id,
|
||||
block_msol_powershell=getattr(
|
||||
auth_policy, "block_msol_power_shell", None
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -2510,12 +2513,15 @@ class DefaultUserRolePermissions(BaseModel):
|
||||
|
||||
|
||||
class AuthorizationPolicy(BaseModel):
|
||||
"""Tenant-wide authorization policy from Microsoft Entra ID."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
default_user_role_permissions: Optional[DefaultUserRolePermissions]
|
||||
guest_invite_settings: Optional[str]
|
||||
guest_user_role_id: Optional[UUID]
|
||||
block_msol_powershell: Optional[bool] = None
|
||||
|
||||
|
||||
# Well-known directory setting template IDs (from /groupSettings).
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
AuthorizationPolicy,
|
||||
DefaultUserRolePermissions,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_policy_msol_powershell_blocked:
|
||||
"""Tests for the entra_policy_msol_powershell_blocked check."""
|
||||
|
||||
def test_authorization_policy_none(self):
|
||||
"""MANUAL when the authorization policy could not be retrieved."""
|
||||
entra_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked import (
|
||||
entra_policy_msol_powershell_blocked,
|
||||
)
|
||||
|
||||
entra_client.authorization_policy = None
|
||||
|
||||
result = entra_policy_msol_powershell_blocked().execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert result[0].resource_id == "authorizationPolicy"
|
||||
assert result[0].resource_name == "Authorization Policy"
|
||||
assert "could not be retrieved" in result[0].status_extended
|
||||
|
||||
def test_block_msol_powershell_none(self):
|
||||
"""MANUAL when blockMsolPowerShell is missing/null in the response."""
|
||||
entra_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked import (
|
||||
entra_policy_msol_powershell_blocked,
|
||||
)
|
||||
|
||||
entra_client.authorization_policy = AuthorizationPolicy(
|
||||
id="authorizationPolicy",
|
||||
name="Authorization Policy",
|
||||
description="",
|
||||
default_user_role_permissions=DefaultUserRolePermissions(),
|
||||
block_msol_powershell=None,
|
||||
)
|
||||
|
||||
result = entra_policy_msol_powershell_blocked().execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert result[0].resource_id == "authorizationPolicy"
|
||||
assert result[0].resource_name == "Authorization Policy"
|
||||
assert "could not be determined" in result[0].status_extended
|
||||
|
||||
def test_block_msol_powershell_true(self):
|
||||
"""PASS when blockMsolPowerShell is true."""
|
||||
entra_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked import (
|
||||
entra_policy_msol_powershell_blocked,
|
||||
)
|
||||
|
||||
entra_client.authorization_policy = AuthorizationPolicy(
|
||||
id="authorizationPolicy",
|
||||
name="Authorization Policy",
|
||||
description="",
|
||||
default_user_role_permissions=DefaultUserRolePermissions(),
|
||||
block_msol_powershell=True,
|
||||
)
|
||||
|
||||
result = entra_policy_msol_powershell_blocked().execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == "authorizationPolicy"
|
||||
assert result[0].resource_name == "Authorization Policy"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Legacy MSOnline (MSOL) PowerShell is blocked in the tenant authorization policy."
|
||||
)
|
||||
|
||||
def test_block_msol_powershell_false(self):
|
||||
"""FAIL when blockMsolPowerShell is false."""
|
||||
entra_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_policy_msol_powershell_blocked.entra_policy_msol_powershell_blocked import (
|
||||
entra_policy_msol_powershell_blocked,
|
||||
)
|
||||
|
||||
entra_client.authorization_policy = AuthorizationPolicy(
|
||||
id="authorizationPolicy",
|
||||
name="Authorization Policy",
|
||||
description="",
|
||||
default_user_role_permissions=DefaultUserRolePermissions(),
|
||||
block_msol_powershell=False,
|
||||
)
|
||||
|
||||
result = entra_policy_msol_powershell_blocked().execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == "authorizationPolicy"
|
||||
assert result[0].resource_name == "Authorization Policy"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Legacy MSOnline (MSOL) PowerShell is not blocked in the tenant authorization policy."
|
||||
)
|
||||
@@ -1990,3 +1990,32 @@ class TestGetUsersError:
|
||||
assert users == {}
|
||||
assert service.users_error is not None
|
||||
assert "Unable to retrieve users from Microsoft Graph" in service.users_error
|
||||
|
||||
|
||||
class TestAuthorizationPolicyMsolMapping:
|
||||
"""``blockMsolPowerShell`` from the Graph SDK model reaches ``AuthorizationPolicy``."""
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False, None])
|
||||
def test_block_msol_powershell_is_mapped(self, value):
|
||||
from msgraph.generated.models.authorization_policy import (
|
||||
AuthorizationPolicy as GraphAuthorizationPolicy,
|
||||
)
|
||||
|
||||
graph_policy = GraphAuthorizationPolicy(
|
||||
id="authorizationPolicy",
|
||||
display_name="Authorization Policy",
|
||||
description="Used to manage authorization related settings across the company.",
|
||||
block_msol_power_shell=value,
|
||||
)
|
||||
service = Entra.__new__(Entra)
|
||||
service.client = SimpleNamespace(
|
||||
policies=SimpleNamespace(
|
||||
authorization_policy=SimpleNamespace(
|
||||
get=AsyncMock(return_value=graph_policy)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
policy = asyncio.run(service._get_authorization_policy())
|
||||
|
||||
assert policy.block_msol_powershell is value
|
||||
Reference in new issue
Block a user