feat(m365): add defenderidentity_health_issues_no_open security check (#10087)

This commit is contained in:
Hugo Pereira Brito
2026-02-19 16:58:08 +01:00
committed by GitHub
parent d2f4f8c406
commit 23e51158e0
12 changed files with 1129 additions and 1 deletions

View File

@@ -42,6 +42,8 @@ When using service principal authentication, add these **Application Permissions
- `AuditLog.Read.All`: Required for Entra service.
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `SecurityIdentitiesHealth.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
- `SecurityIdentitiesSensors.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
**External API Permissions:**
@@ -106,6 +108,8 @@ Browser and Azure CLI authentication methods limit scanning capabilities to chec
- `AuditLog.Read.All`: Required for Entra service
- `Directory.Read.All`: Required for all services
- `Policy.Read.All`: Required for all services
- `SecurityIdentitiesHealth.Read.All`: Required for `defenderidentity_health_issues_no_open` check
- `SecurityIdentitiesSensors.Read.All`: Required for `defenderidentity_health_issues_no_open` check
- `SharePointTenantSettings.Read.All`: Required for SharePoint service
![Permission Screenshots](/images/providers/directory-permission.png)

View File

@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)

View File

@@ -117,6 +117,7 @@
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_safelinks_policy_enabled",
"defender_zap_for_teams_enabled",
"defender_identity_health_issues_no_open",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled"
@@ -715,7 +716,8 @@
"Checks": [
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled"
"defender_malware_policy_notifications_internal_users_malware_enabled",
"defender_identity_health_issues_no_open"
]
},
{

View File

@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
DefenderIdentity,
)
defenderidentity_client = DefenderIdentity(Provider.get_global_provider())

View File

@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "defenderidentity_health_issues_no_open",
"CheckTitle": "Defender for Identity has no unresolved health issues affecting hybrid infrastructure monitoring",
"CheckType": [],
"ServiceName": "defenderidentity",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Defender for Identity Health Issue",
"ResourceGroup": "security",
"Description": "Microsoft Defender for Identity (MDI) monitors your hybrid identity infrastructure and detects advanced threats targeting Active Directory. This check verifies that MDI sensors are deployed and that there are no unresolved health issues that may affect the ability to detect identity-based attacks.",
"Risk": "Without deployed MDI sensors or with unresolved health issues, organizations face critical gaps in threat detection. Misconfigured or missing sensors fail to monitor domain controllers, allowing identity-based attacks like Pass-the-Hash, Golden Ticket, or lateral movement to go undetected. Attackers commonly exploit these blind spots to compromise hybrid environments while evading detection.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/defender-for-identity/health-alerts",
"https://learn.microsoft.com/en-us/graph/api/security-identitycontainer-list-healthissues"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Defender XDR portal at https://security.microsoft.com/\n2. Go to Settings > Identities > Health issues\n3. Review each open health issue and its recommendations\n4. Follow the specific remediation steps provided for each issue\n5. Verify the issue is resolved and status changes to closed",
"Terraform": ""
},
"Recommendation": {
"Text": "Regularly monitor and resolve Defender for Identity health issues to maintain comprehensive visibility into identity-based threats across your hybrid infrastructure.",
"Url": "https://hub.prowler.com/check/defenderidentity_health_issues_no_open"
}
},
"Categories": [
"e5"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check requires SecurityIdentitiesHealth.Read.All permission and a hybrid identity environment with Active Directory on-premises connected to Microsoft Defender for Identity. Health issues can be global (domain-related, such as Directory Services account issues or auditing misconfigurations) or sensor-specific. If no hybrid AD environment is configured, this check will pass with no health issues detected, as MDI only monitors on-premises Active Directory infrastructure."
}

View File

@@ -0,0 +1,140 @@
"""Check for open health issues in Microsoft Defender for Identity.
This module provides a security check that verifies there are no unresolved
health issues in the Microsoft Defender for Identity deployment.
"""
from typing import List
from prowler.lib.check.models import Check, CheckReportM365, Severity
from prowler.providers.m365.services.defenderidentity.defenderidentity_client import (
defenderidentity_client,
)
class defenderidentity_health_issues_no_open(Check):
"""Ensure Microsoft Defender for Identity has no unresolved health issues.
This check evaluates whether there are open health issues in the MDI deployment
that require attention to maintain proper hybrid identity protection.
- PASS: The health issue has been resolved (status is not open).
- FAIL: The health issue is open and requires attention.
- FAIL: No sensors are deployed (MDI cannot protect the environment).
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for open MDI health issues.
This method iterates through all health issues from Microsoft Defender
for Identity and reports on their status. Open issues indicate potential
configuration problems or sensor health concerns that need resolution.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
# Check sensors first - None means API error, empty list means no sensors
sensors_api_failed = defenderidentity_client.sensors is None
health_issues_api_failed = defenderidentity_client.health_issues is None
has_sensors = (
defenderidentity_client.sensors and len(defenderidentity_client.sensors) > 0
)
# If both APIs failed, it's likely a permission issue
if sensors_api_failed and health_issues_api_failed:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Defender for Identity",
resource_id="defenderIdentity",
)
report.status = "FAIL"
report.status_extended = (
"Defender for Identity APIs are not accessible. "
"Ensure the Service Principal has SecurityIdentitiesSensors.Read.All and "
"SecurityIdentitiesHealth.Read.All permissions granted."
)
findings.append(report)
return findings
# If only health issues API failed but we have sensors
if health_issues_api_failed and has_sensors:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Defender for Identity",
resource_id="defenderIdentity",
)
report.status = "FAIL"
report.status_extended = (
f"Cannot read health issues from Defender for Identity "
f"(found {len(defenderidentity_client.sensors)} sensor(s) deployed). "
"Ensure the Service Principal has SecurityIdentitiesHealth.Read.All permission."
)
findings.append(report)
return findings
# If no sensors are deployed (empty list, not None), MDI cannot monitor
if not has_sensors and not sensors_api_failed:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Defender for Identity",
resource_id="defenderIdentity",
)
report.status = "FAIL"
report.status_extended = (
"No sensors deployed in Defender for Identity. "
"Without sensors, MDI cannot monitor health issues in the environment. "
"Deploy sensors on domain controllers to enable protection."
)
findings.append(report)
return findings
# If health_issues is empty list - no issues exist, this is compliant
if not defenderidentity_client.health_issues:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Defender for Identity",
resource_id="defenderIdentity",
)
report.status = "PASS"
report.status_extended = (
"No open health issues found in Defender for Identity."
)
findings.append(report)
return findings
for health_issue in defenderidentity_client.health_issues:
report = CheckReportM365(
metadata=self.metadata(),
resource=health_issue,
resource_name=health_issue.display_name,
resource_id=health_issue.id,
)
issue_type = health_issue.health_issue_type or "unknown"
severity = health_issue.severity or "unknown"
status = (health_issue.status or "").lower()
if status != "open":
report.status = "PASS"
report.status_extended = f"Defender for Identity {issue_type} health issue {health_issue.display_name} is resolved."
else:
report.status = "FAIL"
report.status_extended = f"Defender for Identity {issue_type} health issue {health_issue.display_name} is open with {severity} severity."
# Adjust severity based on issue severity
if severity == "high":
report.check_metadata.Severity = Severity.high
elif severity == "medium":
report.check_metadata.Severity = Severity.medium
elif severity == "low":
report.check_metadata.Severity = Severity.low
findings.append(report)
return findings

View File

@@ -0,0 +1,292 @@
"""Microsoft Defender for Identity service module.
This module provides the DefenderIdentity service class for interacting with
Microsoft Defender for Identity (MDI) APIs, including health issues and sensors.
"""
import asyncio
from typing import List, Optional
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.m365.lib.service.service import M365Service
from prowler.providers.m365.m365_provider import M365Provider
class DefenderIdentity(M365Service):
"""Microsoft Defender for Identity service class.
This class provides methods to retrieve and manage Microsoft Defender for Identity
health issues, which monitor the health status of MDI configuration and sensors.
Attributes:
health_issues (list[HealthIssue]): List of health issues from MDI.
sensors (list[Sensor]): List of sensors from MDI.
"""
def __init__(self, provider: M365Provider):
"""Initialize the DefenderIdentity service client.
Args:
provider: The M365Provider instance for authentication and configuration.
"""
super().__init__(provider)
self.sensors: Optional[List[Sensor]] = []
self.health_issues: Optional[List[HealthIssue]] = []
created_loop = False
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
created_loop = True
if loop.is_closed():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
created_loop = True
if loop.is_running():
raise RuntimeError(
"Cannot initialize DefenderIdentity service while event loop is running"
)
self.sensors = loop.run_until_complete(self._get_sensors())
self.health_issues = loop.run_until_complete(self._get_health_issues())
if created_loop:
asyncio.set_event_loop(None)
loop.close()
async def _get_sensors(self) -> Optional[List["Sensor"]]:
"""Retrieve sensors from Microsoft Defender for Identity.
This method fetches all MDI sensors deployed in the environment,
including their health status and configuration.
Returns:
Optional[List[Sensor]]: A list of sensors from MDI,
or None if the API call failed (tenant not onboarded or missing permissions).
"""
logger.info("DefenderIdentity - Getting sensors...")
sensors: Optional[List[Sensor]] = []
# Step 1: Call the API
try:
sensors_response = await self.client.security.identities.sensors.get()
except Exception as error:
error_msg = str(error)
if "403" in error_msg or "Forbidden" in error_msg:
logger.error(
"DefenderIdentity - Permission denied accessing sensors API. "
"Ensure the Service Principal has SecurityIdentitiesSensors.Read.All permission."
)
elif "401" in error_msg or "Unauthorized" in error_msg:
logger.error(
"DefenderIdentity - Authentication failed accessing sensors API. "
"Verify the Service Principal credentials are valid."
)
else:
logger.error(
f"DefenderIdentity - API error getting sensors: "
f"{error.__class__.__name__}: {error}"
)
return None
# Step 2: Parse the response
try:
while sensors_response:
for sensor in getattr(sensors_response, "value", []) or []:
sensors.append(
Sensor(
id=getattr(sensor, "id", ""),
display_name=getattr(sensor, "display_name", ""),
sensor_type=(
str(getattr(sensor, "sensor_type", ""))
if getattr(sensor, "sensor_type", None)
else None
),
deployment_status=(
str(getattr(sensor, "deployment_status", ""))
if getattr(sensor, "deployment_status", None)
else None
),
health_status=(
str(getattr(sensor, "health_status", ""))
if getattr(sensor, "health_status", None)
else None
),
open_health_issues_count=getattr(
sensor, "open_health_issues_count", 0
)
or 0,
domain_name=getattr(sensor, "domain_name", ""),
version=getattr(sensor, "version", ""),
created_date_time=str(
getattr(sensor, "created_date_time", "")
),
)
)
next_link = getattr(sensors_response, "odata_next_link", None)
if not next_link:
break
sensors_response = (
await self.client.security.identities.sensors.with_url(
next_link
).get()
)
except Exception as error:
logger.error(
f"DefenderIdentity - Error parsing sensors response: "
f"{error.__class__.__name__}: {error}"
)
return None
return sensors
async def _get_health_issues(self) -> Optional[List["HealthIssue"]]:
"""Retrieve health issues from Microsoft Defender for Identity.
This method fetches all health issues from the MDI deployment including
both global and sensor-specific health alerts.
Returns:
Optional[List[HealthIssue]]: A list of health issues from MDI,
or None if the API call failed (tenant not onboarded or missing permissions).
"""
logger.info("DefenderIdentity - Getting health issues...")
health_issues: Optional[List[HealthIssue]] = []
# Step 1: Call the API
try:
health_issues_response = (
await self.client.security.identities.health_issues.get()
)
except Exception as error:
error_msg = str(error)
if "403" in error_msg or "Forbidden" in error_msg:
logger.error(
"DefenderIdentity - Permission denied accessing health issues API. "
"Ensure the Service Principal has SecurityIdentitiesHealth.Read.All permission."
)
elif "401" in error_msg or "Unauthorized" in error_msg:
logger.error(
"DefenderIdentity - Authentication failed accessing health issues API. "
"Verify the Service Principal credentials are valid."
)
else:
logger.error(
f"DefenderIdentity - API error getting health issues: "
f"{error.__class__.__name__}: {error}"
)
return None
# Step 2: Parse the response
try:
while health_issues_response:
for issue in getattr(health_issues_response, "value", []) or []:
health_issues.append(
HealthIssue(
id=getattr(issue, "id", ""),
display_name=getattr(issue, "display_name", ""),
description=getattr(issue, "description", ""),
health_issue_type=getattr(issue, "health_issue_type", None),
severity=getattr(issue, "severity", None),
status=getattr(issue, "status", None),
created_date_time=str(
getattr(issue, "created_date_time", "")
),
last_modified_date_time=str(
getattr(issue, "last_modified_date_time", "")
),
domain_names=getattr(issue, "domain_names", []) or [],
sensor_dns_names=getattr(issue, "sensor_d_n_s_names", [])
or [],
issue_type_id=getattr(issue, "issue_type_id", None),
recommendations=getattr(issue, "recommendations", []) or [],
additional_information=getattr(
issue, "additional_information", []
)
or [],
)
)
next_link = getattr(health_issues_response, "odata_next_link", None)
if not next_link:
break
health_issues_response = (
await self.client.security.identities.health_issues.with_url(
next_link
).get()
)
except Exception as error:
logger.error(
f"DefenderIdentity - Error parsing health issues response: "
f"{error.__class__.__name__}: {error}"
)
return None
return health_issues
class Sensor(BaseModel):
"""Model for Microsoft Defender for Identity sensor.
Attributes:
id: The unique identifier for the sensor.
display_name: The display name of the sensor.
sensor_type: The type of sensor (domainControllerIntegrated, domainControllerStandalone, adfsIntegrated).
deployment_status: The deployment status (upToDate, outdated, updating, updateFailed, notConfigured).
health_status: The health status of the sensor (healthy, notHealthyLow, notHealthyMedium, notHealthyHigh).
open_health_issues_count: Number of open health issues for this sensor.
domain_name: The domain name the sensor is monitoring.
version: The version of the sensor.
created_date_time: When the sensor was created.
"""
id: str
display_name: str
sensor_type: Optional[str]
deployment_status: Optional[str]
health_status: Optional[str]
open_health_issues_count: int
domain_name: str
version: str
created_date_time: str
class HealthIssue(BaseModel):
"""Model for Microsoft Defender for Identity health issue.
Attributes:
id: The unique identifier for the health issue.
display_name: The display name of the health issue.
description: A detailed description of the health issue.
health_issue_type: The type of health issue (global or sensor).
severity: The severity level of the issue (low, medium, high).
status: The current status of the issue (open, closed).
created_date_time: When the issue was created.
last_modified_date_time: When the issue was last modified.
domain_names: List of domain names affected by the issue.
sensor_dns_names: List of sensor DNS names affected by the issue.
issue_type_id: The type identifier for the issue.
recommendations: List of recommended actions to resolve the issue.
additional_information: Additional information about the issue.
"""
id: str
display_name: str
description: str
health_issue_type: Optional[str]
severity: Optional[str]
status: Optional[str]
created_date_time: str
last_modified_date_time: str
domain_names: List[str]
sensor_dns_names: List[str]
issue_type_id: Optional[str]
recommendations: List[str]
additional_information: List[str]

View File

@@ -0,0 +1,646 @@
from unittest import mock
from prowler.lib.check.models import Severity
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
def create_mock_sensor():
"""Create a mock sensor for testing."""
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
Sensor,
)
return Sensor(
id="test-sensor-id",
display_name="Test Sensor",
sensor_type="domainControllerIntegrated",
deployment_status="upToDate",
health_status="healthy",
open_health_issues_count=0,
domain_name="example.com",
version="2.200.0.0",
created_date_time="2024-01-01T00:00:00Z",
)
class Test_defenderidentity_health_issues_no_open:
def test_no_health_issues_with_sensors(self):
"""Test when there are no health issues but sensors are deployed: expected PASS."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
defenderidentity_client.health_issues = []
defenderidentity_client.sensors = [create_mock_sensor()]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "No open health issues found in Defender for Identity."
)
assert result[0].resource == {}
assert result[0].resource_name == "Defender for Identity"
assert result[0].resource_id == "defenderIdentity"
def test_no_sensors_deployed(self):
"""Test when no sensors are deployed: expected FAIL."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
defenderidentity_client.health_issues = []
defenderidentity_client.sensors = []
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "No sensors deployed" in result[0].status_extended
assert result[0].resource == {}
assert result[0].resource_name == "Defender for Identity"
assert result[0].resource_id == "defenderIdentity"
def test_both_apis_failed(self):
"""Test when both sensors and health_issues APIs fail (None): expected FAIL with permission message."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
defenderidentity_client.health_issues = None
defenderidentity_client.sensors = None
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "APIs are not accessible" in result[0].status_extended
assert "SecurityIdentitiesSensors.Read.All" in result[0].status_extended
assert "SecurityIdentitiesHealth.Read.All" in result[0].status_extended
assert result[0].resource == {}
assert result[0].resource_name == "Defender for Identity"
assert result[0].resource_id == "defenderIdentity"
def test_health_issues_api_failed_but_sensors_exist(self):
"""Test when health_issues API fails but sensors exist: expected FAIL with specific permission message."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
defenderidentity_client.health_issues = None
defenderidentity_client.sensors = [create_mock_sensor()]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "Cannot read health issues" in result[0].status_extended
assert "1 sensor(s) deployed" in result[0].status_extended
assert "SecurityIdentitiesHealth.Read.All" in result[0].status_extended
assert result[0].resource == {}
assert result[0].resource_name == "Defender for Identity"
assert result[0].resource_id == "defenderIdentity"
def test_health_issue_resolved(self):
"""Test when a health issue has been resolved (status is not open)."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-1"
health_issue_name = "Test Health Issue Resolved"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A test health issue that has been resolved",
health_issue_type="sensor",
severity="medium",
status="closed",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=["sensor1.example.com"],
issue_type_id="test-issue-type-1",
recommendations=["Fix the issue"],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Defender for Identity sensor health issue {health_issue_name} is resolved."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name
def test_health_issue_open_high_severity(self):
"""Test when a health issue is open with high severity."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-2"
health_issue_name = "Critical Sensor Health Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A critical health issue that is open",
health_issue_type="global",
severity="high",
status="open",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=[],
issue_type_id="test-issue-type-2",
recommendations=["Fix the critical issue immediately"],
additional_information=["Additional info about the issue"],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Defender for Identity global health issue {health_issue_name} is open with high severity."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name
assert result[0].check_metadata.Severity == Severity.high
def test_health_issue_open_medium_severity(self):
"""Test when a health issue is open with medium severity."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-3"
health_issue_name = "Medium Severity Sensor Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A medium severity health issue",
health_issue_type="sensor",
severity="medium",
status="open",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=["sensor2.example.com"],
issue_type_id="test-issue-type-3",
recommendations=["Review and fix the issue"],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Defender for Identity sensor health issue {health_issue_name} is open with medium severity."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name
assert result[0].check_metadata.Severity == Severity.medium
def test_health_issue_open_low_severity(self):
"""Test when a health issue is open with low severity."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-4"
health_issue_name = "Low Severity Health Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A low severity health issue",
health_issue_type="global",
severity="low",
status="open",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=[],
issue_type_id="test-issue-type-4",
recommendations=["Consider fixing the issue"],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Defender for Identity global health issue {health_issue_name} is open with low severity."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name
assert result[0].check_metadata.Severity == Severity.low
def test_multiple_health_issues_mixed_status(self):
"""Test when there are multiple health issues with different statuses."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id="issue-1",
display_name="Resolved Issue",
description="A resolved health issue",
health_issue_type="sensor",
severity="high",
status="closed",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=["sensor1.example.com"],
issue_type_id="type-1",
recommendations=[],
additional_information=[],
),
HealthIssue(
id="issue-2",
display_name="Open Issue",
description="An open health issue",
health_issue_type="global",
severity="medium",
status="open",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=[],
issue_type_id="type-2",
recommendations=["Fix this issue"],
additional_information=[],
),
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 2
# First result should be PASS (resolved issue)
assert result[0].status == "PASS"
assert result[0].resource_id == "issue-1"
assert result[0].resource_name == "Resolved Issue"
assert (
result[0].status_extended
== "Defender for Identity sensor health issue Resolved Issue is resolved."
)
# Second result should be FAIL (open issue)
assert result[1].status == "FAIL"
assert result[1].resource_id == "issue-2"
assert result[1].resource_name == "Open Issue"
assert (
result[1].status_extended
== "Defender for Identity global health issue Open Issue is open with medium severity."
)
assert result[1].check_metadata.Severity == Severity.medium
def test_health_issue_with_unknown_type_and_severity(self):
"""Test when health issue has None/unknown type and severity."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-5"
health_issue_name = "Unknown Type Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A health issue with unknown type and severity",
health_issue_type=None,
severity=None,
status="open",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=[],
sensor_dns_names=[],
issue_type_id="test-issue-type-5",
recommendations=[],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Defender for Identity unknown health issue {health_issue_name} is open with unknown severity."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name
def test_health_issue_status_case_insensitive(self):
"""Test that status comparison is case insensitive (OPEN vs open)."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-6"
health_issue_name = "Uppercase Status Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A health issue with uppercase OPEN status",
health_issue_type="sensor",
severity="high",
status="OPEN",
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=["example.com"],
sensor_dns_names=["sensor.example.com"],
issue_type_id="test-issue-type-6",
recommendations=["Fix the issue"],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Defender for Identity sensor health issue {health_issue_name} is open with high severity."
)
assert result[0].resource_id == health_issue_id
def test_health_issue_with_empty_status(self):
"""Test when health issue has empty/None status (treated as not open)."""
defenderidentity_client = mock.MagicMock()
defenderidentity_client.audited_tenant = "audited_tenant"
defenderidentity_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.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open.defenderidentity_client",
new=defenderidentity_client,
),
):
from prowler.providers.m365.services.defenderidentity.defenderidentity_health_issues_no_open.defenderidentity_health_issues_no_open import (
defenderidentity_health_issues_no_open,
)
from prowler.providers.m365.services.defenderidentity.defenderidentity_service import (
HealthIssue,
)
health_issue_id = "test-health-issue-id-7"
health_issue_name = "Empty Status Issue"
defenderidentity_client.sensors = [create_mock_sensor()]
defenderidentity_client.health_issues = [
HealthIssue(
id=health_issue_id,
display_name=health_issue_name,
description="A health issue with empty status",
health_issue_type="global",
severity="medium",
status=None,
created_date_time="2024-01-01T00:00:00Z",
last_modified_date_time="2024-01-02T00:00:00Z",
domain_names=[],
sensor_dns_names=[],
issue_type_id="test-issue-type-7",
recommendations=[],
additional_information=[],
)
]
check = defenderidentity_health_issues_no_open()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Defender for Identity global health issue {health_issue_name} is resolved."
)
assert result[0].resource_id == health_issue_id
assert result[0].resource_name == health_issue_name