Skip to content

Commit 4b40653

Browse files
authored
SecOps 41018 - No open Microsoft Defender for Identity health issues are present in the tenant (#1352)
2 parents 159941e + 06616cf commit 4b40653

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft Defender for Identity health issues are the service's self-diagnostic surface: open issues such as "Sensor not communicating", "Domain Synchronizer not running", and "Workspace is not receiving directory replication traffic" indicate that a sensor either cannot reach the cloud service, cannot capture the network traffic it needs, or cannot read the directory objects it needs to evaluate. Each unresolved health issue narrows detection coverage on the affected scope. When a sensor is degraded, adversary activity against the assets it monitors may proceed without generating the alerts the security operations team expects, undermining the investment in the platform. The risk compounds when multiple sensors report issues simultaneously, because an attacker who moves between monitored segments can cross from one blind spot into another without triggering a detection at any point in the chain. Open health issues must therefore be triaged and resolved as part of routine operations; any open issue at medium or high severity is a coverage gap that requires action.
2+
3+
**Remediation action**
4+
5+
- [Microsoft Defender for Identity health issues](https://learn.microsoft.com/en-us/defender-for-identity/health-alerts)
6+
- [Troubleshoot Microsoft Defender for Identity known issues](https://learn.microsoft.com/en-us/defender-for-identity/troubleshooting-known-issues)
7+
- [Microsoft Defender for Identity sensor settings](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-sensor-settings)
8+
9+
<!--- Results --->
10+
%TestResult%
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<#
2+
.SYNOPSIS
3+
No open Microsoft Defender for Identity health issues are present in the tenant.
4+
5+
.NOTES
6+
Test ID: 41018
7+
Workshop Task: SECOPS-018
8+
Pillar: SecOps
9+
Category: Identity threat protection
10+
Required permission: SecurityIdentitiesHealth.Read.All
11+
#>
12+
13+
function Test-Assessment-41018 {
14+
[ZtTest(
15+
Category = 'Identity threat protection',
16+
CompatibleLicense = ('ATA'),
17+
ImplementationCost = 'Low',
18+
Pillar = 'SecOps',
19+
RiskLevel = 'High',
20+
Service = ('Graph'),
21+
SfiPillar = 'Monitor and detect cyberthreats',
22+
TenantType = ('Workforce'),
23+
TestId = 41018,
24+
Title = 'No open Microsoft Defender for Identity health issues are present in the tenant',
25+
UserImpact = 'Low'
26+
)]
27+
[CmdletBinding()]
28+
param()
29+
30+
#region Data Collection
31+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
32+
33+
$activity = 'Checking Microsoft Defender for Identity health issues'
34+
$queryError = $null
35+
$healthIssues = $null
36+
37+
Write-ZtProgress -Activity $activity -Status 'Querying MDI health issues'
38+
39+
try {
40+
# Q1: List all open MDI health issues.
41+
$healthIssues = Invoke-ZtGraphRequest -RelativeUri 'security/identities/healthIssues' -Filter "status eq 'open'" -ApiVersion beta -ErrorAction Stop
42+
}
43+
catch {
44+
$queryError = $_
45+
Write-PSFMessage "Failed to retrieve MDI health issues: $_" -Tag Test -Level Warning
46+
}
47+
#endregion Data Collection
48+
49+
#region Assessment Logic
50+
$investigateParams = @{
51+
TestId = '41018'
52+
Title = 'No open Microsoft Defender for Identity health issues are present in the tenant'
53+
Status = $false
54+
Result = '⚠️ Microsoft Defender for Identity is deployed but the healthIssues collection returned an error.'
55+
CustomStatus = 'Investigate'
56+
}
57+
58+
if ($queryError) {
59+
$httpStatus = Get-ZtHttpStatusCode -ErrorRecord $queryError
60+
61+
# 403 → Parse error code to distinguish permission denied from not-onboarded.
62+
# - "UnknownError": permission missing (SecurityIdentitiesHealth.Read.All not consented) → Investigate.
63+
# - "Forbidden" or unparseable: MDI not onboarded → NotApplicable.
64+
if ($httpStatus -eq 403) {
65+
$errorCode = $null
66+
try {
67+
$errStr = $queryError.ToString()
68+
if ($errStr -match '(\{"error".*\})') {
69+
$errorCode = ($Matches[1] | ConvertFrom-Json).error.code
70+
}
71+
}
72+
catch {
73+
# Parse failure; treat as not onboarded.
74+
Write-PSFMessage "Failed to parse error response; treating as MDI not onboarded." -Tag Test -Level VeryVerbose
75+
}
76+
77+
if ($errorCode -eq 'UnknownError') {
78+
Add-ZtTestResultDetail @investigateParams
79+
return
80+
}
81+
# Fall through to NotApplicable check below.
82+
}
83+
84+
# 404 or 403 (non-UnknownError) → MDI not onboarded.
85+
if ($httpStatus -in (403, 404)) {
86+
Add-ZtTestResultDetail -SkippedBecause NotApplicable
87+
return
88+
}
89+
90+
# 401, 5xx, or any other error → Investigate.
91+
Add-ZtTestResultDetail @investigateParams
92+
return
93+
}
94+
95+
$healthIssues = @($healthIssues)
96+
97+
# unknownFutureValue in severity signals API schema drift → Investigate.
98+
$unknownItems = @($healthIssues | Where-Object { $_.severity -eq 'unknownFutureValue' })
99+
if ($unknownItems.Count -gt 0) {
100+
Add-ZtTestResultDetail @investigateParams
101+
return
102+
}
103+
104+
# Low-severity issues are reported but do not fail the check; only medium/high do.
105+
$criticalIssues = @($healthIssues | Where-Object { $_.severity -in ('medium', 'high') })
106+
$passed = $criticalIssues.Count -eq 0
107+
108+
if ($passed) {
109+
$testResultMarkdown = "✅ No medium- or high-severity Microsoft Defender for Identity health issues are open in the tenant.`n`n%TestResult%"
110+
}
111+
else {
112+
$testResultMarkdown = "❌ One or more medium- or high-severity Microsoft Defender for Identity health issues are open and reduce detection coverage.`n`n%TestResult%"
113+
}
114+
#endregion Assessment Logic
115+
116+
#region Report Generation
117+
$healthPageUrl = 'https://security.microsoft.com/securitysettings/identities'
118+
$mdInfo = ''
119+
120+
if ($healthIssues.Count -gt 0) {
121+
$severityOrder = @{ high = 0; medium = 1; low = 2; unknownFutureValue = 3 }
122+
$sortedIssues = @($healthIssues | Sort-Object { $severityOrder[$_.severity] }, displayName)
123+
$maxDisplay = 10
124+
$totalCount = $sortedIssues.Count
125+
$displayIssues = if ($totalCount -gt $maxDisplay) { $sortedIssues | Select-Object -First $maxDisplay } else { $sortedIssues }
126+
127+
$tableRows = ''
128+
foreach ($issue in $displayIssues) {
129+
$displayName = Get-SafeMarkdown $issue.displayName
130+
$severity = $issue.severity
131+
$issueType = $issue.healthIssueType
132+
$sensors = if ($issue.sensorDNSNames -and $issue.sensorDNSNames.Count -gt 0) { $issue.sensorDNSNames -join ', ' } else { '' }
133+
$domains = if ($issue.domainNames -and $issue.domainNames.Count -gt 0) { $issue.domainNames -join ', ' } else { '' }
134+
$lastModified = if ($issue.lastModifiedDateTime) { Get-FormattedDate -DateString $issue.lastModifiedDateTime } else { '' }
135+
$rowStatus = if ($issue.severity -in ('medium', 'high')) { '❌ Fail' } else { '✅ Pass' }
136+
$tableRows += "| $displayName | $severity | $issueType | $sensors | $domains | $lastModified | $rowStatus |`n"
137+
}
138+
139+
if ($totalCount -gt $maxDisplay) {
140+
$remaining = $totalCount - $maxDisplay
141+
$tableRows += "| ... | ... | ... | ... | ... | ... | ... |`n"
142+
}
143+
144+
# Show count above the table when results are truncated.
145+
$preTableLines = ''
146+
if ($totalCount -gt $maxDisplay) {
147+
$preTableLines = "Total open issues: $totalCount`n`n"
148+
}
149+
150+
$formatTemplate = @'
151+
152+
153+
### [Defender XDR > Settings > Identities > Health issues]({0})
154+
155+
{1}| Display name | Severity | Type | Affected sensors | Affected domains | Last modified | Status |
156+
| :----------- | :------- | :--- | :--------------- | :--------------- | :------------ | :----- |
157+
{2}
158+
'@
159+
$mdInfo = $formatTemplate -f $healthPageUrl, $preTableLines, $tableRows
160+
}
161+
162+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
163+
#endregion Report Generation
164+
165+
$params = @{
166+
TestId = '41018'
167+
Title = 'No open Microsoft Defender for Identity health issues are present in the tenant'
168+
Status = $passed
169+
Result = $testResultMarkdown
170+
}
171+
Add-ZtTestResultDetail @params
172+
}

0 commit comments

Comments
 (0)