mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-16 17:22:43 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42ea4417be |
@@ -4,6 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.24.1] (Prowler v5.24.1)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `entra_pim_role_usage_alert_exists` check for m365 provider [(#10799)](https://github.com/prowler-cloud/prowler/pull/10799)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- `msgraph-sdk` from 1.23.0 to 1.55.0 and `azure-mgmt-resource` from 23.3.0 to 24.0.0, removing `marshmallow` as is a transitively dev dependency [(#10733)](https://github.com/prowler-cloud/prowler/pull/10733)
|
||||
|
||||
@@ -1502,7 +1502,9 @@
|
||||
{
|
||||
"Id": "5.3.1",
|
||||
"Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Organizations should remove permanent members from privileged Office 365 roles and instead make them eligible, through a JIT activation workflow.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"entra_pim_role_usage_alert_exists"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "5 Microsoft Entra admin center",
|
||||
|
||||
@@ -1803,7 +1803,9 @@
|
||||
{
|
||||
"Id": "5.3.1",
|
||||
"Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Organizations should remove permanent members from privileged Office 365 roles and instead make them eligible, through a JIT activation workflow. Ensure 'Privileged Identity Management' is used to manage roles.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"entra_pim_role_usage_alert_exists"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "5 Microsoft Entra admin center",
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
"Checks": [
|
||||
"entra_admin_portals_access_restriction",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_pim_role_usage_alert_exists",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"sharepoint_external_sharing_managed",
|
||||
"sharepoint_external_sharing_restricted",
|
||||
@@ -672,6 +673,7 @@
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_pim_role_usage_alert_exists",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
"entra_seamless_sso_disabled"
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_pim_role_usage_alert_exists",
|
||||
"CheckTitle": "PIM alert for unused privileged roles monitors stale role assignments",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Privileged Identity Management (PIM) can be configured to alert when **privileged roles are not being used**. This alert detects stale role assignments where administrators have not exercised their assigned privileges, helping identify unnecessary access that should be reviewed or removed.",
|
||||
"Risk": "Without monitoring for unused privileged roles, **stale role assignments** accumulate undetected. Dormant privileged accounts become targets for **credential theft** and **lateral movement**, expanding the attack surface. Attackers who compromise an unused admin account gain elevated access with lower detection risk.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-configure-security-alerts",
|
||||
"https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-security-alerts"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. In the Microsoft Entra admin center, go to Identity governance > Privileged Identity Management > Microsoft Entra roles > Alerts.\n2. Locate the alert **Administrators aren't using their privileged roles**.\n3. Click the alert to review its settings and ensure it is enabled.\n4. Configure the alert threshold and notification settings as needed.\n5. Review any flagged stale role assignments and remove unnecessary access.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable PIM alerts for **unused privileged roles** to detect stale assignments. Apply the **principle of least privilege** by regularly reviewing role assignments and removing access that is no longer needed. Use **time-bound eligible assignments** instead of permanent active roles.",
|
||||
"Url": "https://hub.prowler.com/check/entra_pim_role_usage_alert_exists"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e5"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
# The alert definition ID for "Administrators aren't using their privileged roles"
|
||||
# (also known as the StaleSignInAlert or inactive role assignment alert).
|
||||
STALE_SIGN_IN_ALERT_DEFINITION_ID = "DirectoryRoleInactiveAlertDefinition"
|
||||
|
||||
|
||||
class entra_pim_role_usage_alert_exists(Check):
|
||||
"""
|
||||
Ensure that the PIM alert for unused privileged roles is configured and active.
|
||||
|
||||
This check verifies that Privileged Identity Management (PIM) is configured
|
||||
to alert when administrators are not using their assigned privileged roles,
|
||||
helping detect stale or unnecessary role assignments.
|
||||
- PASS: The PIM alert for unused privileged roles exists and is active.
|
||||
- FAIL: The PIM alert for unused privileged roles does not exist or is not active.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""Execute the check logic.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="PIM Role Usage Alert",
|
||||
resource_id="pimRoleUsageAlert",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "PIM alert for unused privileged roles does not exist or is not active."
|
||||
|
||||
for alert in entra_client.pim_alerts:
|
||||
if (
|
||||
STALE_SIGN_IN_ALERT_DEFINITION_ID
|
||||
in alert.alert_definition_id
|
||||
and alert.is_active
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = "PIM alert for unused privileged roles exists and is active."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -36,6 +36,7 @@ class Entra(M365Service):
|
||||
user_accounts_status (dict): Dictionary of user account statuses.
|
||||
oauth_apps (dict): Dictionary of OAuth applications from Defender XDR.
|
||||
authentication_method_configurations (dict): Dictionary of authentication method configurations.
|
||||
pim_alerts (list): List of PIM alerts configured in the tenant.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: M365Provider):
|
||||
@@ -83,6 +84,7 @@ class Entra(M365Service):
|
||||
self._get_oauth_apps(),
|
||||
self._get_directory_sync_settings(),
|
||||
self._get_authentication_method_configurations(),
|
||||
self._get_pim_alerts(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -98,6 +100,7 @@ class Entra(M365Service):
|
||||
self.authentication_method_configurations: Dict[
|
||||
str, AuthenticationMethodConfiguration
|
||||
] = attributes[9]
|
||||
self.pim_alerts: List[PIMAlert] = attributes[10]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
@@ -1019,6 +1022,45 @@ OAuthAppInfo
|
||||
|
||||
return oauth_apps
|
||||
|
||||
async def _get_pim_alerts(self):
|
||||
"""Retrieve PIM (Privileged Identity Management) alerts from Microsoft Entra.
|
||||
|
||||
Fetches PIM alerts from the identity governance API to determine which
|
||||
alert policies are configured and active in the tenant, including
|
||||
alerts for unused privileged roles.
|
||||
|
||||
Returns:
|
||||
list[PIMAlert]: A list of PIM alerts configured in the tenant,
|
||||
or an empty list if retrieval fails.
|
||||
"""
|
||||
logger.info("Entra - Getting PIM alerts...")
|
||||
pim_alerts = []
|
||||
try:
|
||||
alerts_response = (
|
||||
await self.client.identity_governance.role_management_alerts.alerts.get()
|
||||
)
|
||||
for alert in getattr(alerts_response, "value", []) or []:
|
||||
pim_alerts.append(
|
||||
PIMAlert(
|
||||
id=getattr(alert, "id", ""),
|
||||
alert_definition_id=getattr(
|
||||
alert, "alert_definition_id", ""
|
||||
),
|
||||
scope_id=getattr(alert, "scope_id", "") or "",
|
||||
scope_type=getattr(alert, "scope_type", "") or "",
|
||||
is_active=getattr(alert, "is_active", False) or False,
|
||||
number_of_affected_items=getattr(
|
||||
alert, "number_of_affected_items", 0
|
||||
)
|
||||
or 0,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return pim_alerts
|
||||
|
||||
async def _get_authentication_method_configurations(self):
|
||||
"""Retrieve authentication method configurations from Microsoft Entra.
|
||||
|
||||
@@ -1481,3 +1523,23 @@ class OAuthApp(BaseModel):
|
||||
is_admin_consented: bool = False
|
||||
last_used_time: Optional[str] = None
|
||||
app_origin: str = ""
|
||||
|
||||
|
||||
class PIMAlert(BaseModel):
|
||||
"""Model representing a PIM (Privileged Identity Management) alert.
|
||||
|
||||
Attributes:
|
||||
id: The unique identifier for the alert.
|
||||
alert_definition_id: The identifier of the alert definition type.
|
||||
scope_id: The scope ID (typically the tenant ID).
|
||||
scope_type: The scope type (e.g., 'DirectoryRole').
|
||||
is_active: Whether the alert is currently active/enabled.
|
||||
number_of_affected_items: The number of items affected by the alert.
|
||||
"""
|
||||
|
||||
id: str
|
||||
alert_definition_id: str
|
||||
scope_id: str = ""
|
||||
scope_type: str = ""
|
||||
is_active: bool = False
|
||||
number_of_affected_items: int = 0
|
||||
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import PIMAlert
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_pim_role_usage_alert_exists:
|
||||
def test_no_pim_alerts(self):
|
||||
"""Test when no PIM alerts exist - should FAIL since the alert is always evaluated."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
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_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists import (
|
||||
entra_pim_role_usage_alert_exists,
|
||||
)
|
||||
|
||||
entra_client.pim_alerts = []
|
||||
|
||||
check = entra_pim_role_usage_alert_exists()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "PIM alert for unused privileged roles does not exist or is not active."
|
||||
)
|
||||
assert result[0].resource_name == "PIM Role Usage Alert"
|
||||
assert result[0].resource_id == "pimRoleUsageAlert"
|
||||
|
||||
def test_entra_pim_role_usage_alert_exists_pass(self):
|
||||
"""Test when the PIM alert for unused privileged roles exists and is active."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
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_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists import (
|
||||
entra_pim_role_usage_alert_exists,
|
||||
)
|
||||
|
||||
entra_client.pim_alerts = [
|
||||
PIMAlert(
|
||||
id="alert-1",
|
||||
alert_definition_id="DirectoryRoleInactiveAlertDefinition",
|
||||
scope_id="tenant-id",
|
||||
scope_type="DirectoryRole",
|
||||
is_active=True,
|
||||
number_of_affected_items=3,
|
||||
),
|
||||
]
|
||||
|
||||
check = entra_pim_role_usage_alert_exists()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "PIM alert for unused privileged roles exists and is active."
|
||||
)
|
||||
assert result[0].resource_name == "PIM Role Usage Alert"
|
||||
assert result[0].resource_id == "pimRoleUsageAlert"
|
||||
|
||||
def test_entra_pim_role_usage_alert_exists_fail_not_active(self):
|
||||
"""Test when the PIM alert for unused privileged roles exists but is not active."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
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_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists import (
|
||||
entra_pim_role_usage_alert_exists,
|
||||
)
|
||||
|
||||
entra_client.pim_alerts = [
|
||||
PIMAlert(
|
||||
id="alert-1",
|
||||
alert_definition_id="DirectoryRoleInactiveAlertDefinition",
|
||||
scope_id="tenant-id",
|
||||
scope_type="DirectoryRole",
|
||||
is_active=False,
|
||||
number_of_affected_items=0,
|
||||
),
|
||||
]
|
||||
|
||||
check = entra_pim_role_usage_alert_exists()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "PIM alert for unused privileged roles does not exist or is not active."
|
||||
)
|
||||
assert result[0].resource_name == "PIM Role Usage Alert"
|
||||
assert result[0].resource_id == "pimRoleUsageAlert"
|
||||
|
||||
def test_entra_pim_role_usage_alert_exists_fail_different_alert(self):
|
||||
"""Test when PIM alerts exist but none match the expected definition ID."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
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_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists import (
|
||||
entra_pim_role_usage_alert_exists,
|
||||
)
|
||||
|
||||
entra_client.pim_alerts = [
|
||||
PIMAlert(
|
||||
id="alert-1",
|
||||
alert_definition_id="SomeOtherAlertDefinition",
|
||||
scope_id="tenant-id",
|
||||
scope_type="DirectoryRole",
|
||||
is_active=True,
|
||||
number_of_affected_items=1,
|
||||
),
|
||||
]
|
||||
|
||||
check = entra_pim_role_usage_alert_exists()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "PIM alert for unused privileged roles does not exist or is not active."
|
||||
)
|
||||
assert result[0].resource_name == "PIM Role Usage Alert"
|
||||
assert result[0].resource_id == "pimRoleUsageAlert"
|
||||
|
||||
def test_entra_pim_role_usage_alert_exists_pass_among_multiple_alerts(self):
|
||||
"""Test when multiple PIM alerts exist and the correct one is active."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
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_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_pim_role_usage_alert_exists.entra_pim_role_usage_alert_exists import (
|
||||
entra_pim_role_usage_alert_exists,
|
||||
)
|
||||
|
||||
entra_client.pim_alerts = [
|
||||
PIMAlert(
|
||||
id="alert-1",
|
||||
alert_definition_id="SomeOtherAlertDefinition",
|
||||
scope_id="tenant-id",
|
||||
scope_type="DirectoryRole",
|
||||
is_active=True,
|
||||
number_of_affected_items=1,
|
||||
),
|
||||
PIMAlert(
|
||||
id="alert-2",
|
||||
alert_definition_id="DirectoryRoleInactiveAlertDefinition",
|
||||
scope_id="tenant-id",
|
||||
scope_type="DirectoryRole",
|
||||
is_active=True,
|
||||
number_of_affected_items=5,
|
||||
),
|
||||
]
|
||||
|
||||
check = entra_pim_role_usage_alert_exists()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "PIM alert for unused privileged roles exists and is active."
|
||||
)
|
||||
assert result[0].resource_name == "PIM Role Usage Alert"
|
||||
assert result[0].resource_id == "pimRoleUsageAlert"
|
||||
Reference in New Issue
Block a user