tests/Test-Assessment.41018.ps1

<#
.SYNOPSIS
    No open Microsoft Defender for Identity health issues are present in the tenant.
 
.NOTES
    Test ID: 41018
    Workshop Task: SECOPS-018
    Pillar: SecOps
    Category: Identity threat protection
    Required permission: SecurityIdentitiesHealth.Read.All
#>


function Test-Assessment-41018 {
    [ZtTest(
        Category = 'Identity threat protection',
        CompatibleLicense = ('ATA'),
        ImplementationCost = 'Low',
        Pillar = 'SecOps',
        RiskLevel = 'High',
        Service = ('Graph'),
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce'),
        TestId = 41018,
        Title = 'No open Microsoft Defender for Identity health issues are present in the tenant',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

    $activity   = 'Checking Microsoft Defender for Identity health issues'
    $queryError = $null
    $healthIssues = $null

    Write-ZtProgress -Activity $activity -Status 'Querying MDI health issues'

    try {
        # Q1: List all open MDI health issues.
        $healthIssues = Invoke-ZtGraphRequest -RelativeUri 'security/identities/healthIssues' -Filter "status eq 'open'" -ApiVersion beta -ErrorAction Stop
    }
    catch {
        $queryError = $_
        Write-PSFMessage "Failed to retrieve MDI health issues: $_" -Tag Test -Level Warning
    }
    #endregion Data Collection

    #region Assessment Logic
    $investigateParams = @{
        TestId       = '41018'
        Title        = 'No open Microsoft Defender for Identity health issues are present in the tenant'
        Status       = $false
        Result       = '⚠️ Microsoft Defender for Identity is deployed but the healthIssues collection returned an error.'
        CustomStatus = 'Investigate'
    }

    if ($queryError) {
        $httpStatus = Get-ZtHttpStatusCode -ErrorRecord $queryError

        # 403 → Parse error code to distinguish permission denied from not-onboarded.
        # - "UnknownError": permission missing (SecurityIdentitiesHealth.Read.All not consented) → Investigate.
        # - "Forbidden" or unparseable: MDI not onboarded → NotApplicable.
        if ($httpStatus -eq 403) {
            $errorCode = $null
            try {
                $errStr = $queryError.ToString()
                if ($errStr -match '(\{"error".*\})') {
                    $errorCode = ($Matches[1] | ConvertFrom-Json).error.code
                }
            }
            catch {
                # Parse failure; treat as not onboarded.
                Write-PSFMessage "Failed to parse error response; treating as MDI not onboarded." -Tag Test -Level VeryVerbose
            }

            if ($errorCode -eq 'UnknownError') {
                Add-ZtTestResultDetail @investigateParams
                return
            }
            # Fall through to NotApplicable check below.
        }

        # 404 or 403 (non-UnknownError) → MDI not onboarded.
        if ($httpStatus -in (403, 404)) {
            Add-ZtTestResultDetail -SkippedBecause NotApplicable
            return
        }

        # 401, 5xx, or any other error → Investigate.
        Add-ZtTestResultDetail @investigateParams
        return
    }

    $healthIssues = @($healthIssues)

    # unknownFutureValue in severity signals API schema drift → Investigate.
    $unknownItems = @($healthIssues | Where-Object { $_.severity -eq 'unknownFutureValue' })
    if ($unknownItems.Count -gt 0) {
        Add-ZtTestResultDetail @investigateParams
        return
    }

    # Low-severity issues are reported but do not fail the check; only medium/high do.
    $criticalIssues = @($healthIssues | Where-Object { $_.severity -in ('medium', 'high') })
    $passed = $criticalIssues.Count -eq 0

    if ($passed) {
        $testResultMarkdown = "✅ No medium- or high-severity Microsoft Defender for Identity health issues are open in the tenant.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "❌ One or more medium- or high-severity Microsoft Defender for Identity health issues are open and reduce detection coverage.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $healthPageUrl = 'https://security.microsoft.com/securitysettings/identities'
    $mdInfo        = ''

    if ($healthIssues.Count -gt 0) {
        $severityOrder = @{ high = 0; medium = 1; low = 2; unknownFutureValue = 3 }
        $sortedIssues  = @($healthIssues | Sort-Object { $severityOrder[$_.severity] }, displayName)
        $maxDisplay    = 10
        $totalCount    = $sortedIssues.Count
        $displayIssues = if ($totalCount -gt $maxDisplay) { $sortedIssues | Select-Object -First $maxDisplay } else { $sortedIssues }

        $tableRows = ''
        foreach ($issue in $displayIssues) {
            $displayName  = Get-SafeMarkdown $issue.displayName
            $severity     = $issue.severity
            $issueType    = $issue.healthIssueType
            $sensors      = if ($issue.sensorDNSNames -and $issue.sensorDNSNames.Count -gt 0) { $issue.sensorDNSNames -join ', ' } else { '—' }
            $domains      = if ($issue.domainNames -and $issue.domainNames.Count -gt 0) { $issue.domainNames -join ', ' } else { '—' }
            $lastModified = if ($issue.lastModifiedDateTime) { Get-FormattedDate -DateString $issue.lastModifiedDateTime } else { '—' }
            $rowStatus    = if ($issue.severity -in ('medium', 'high')) { '❌ Fail' } else { '✅ Pass' }
            $tableRows   += "| $displayName | $severity | $issueType | $sensors | $domains | $lastModified | $rowStatus |`n"
        }

        if ($totalCount -gt $maxDisplay) {
            $remaining  = $totalCount - $maxDisplay
            $tableRows += "| ... | ... | ... | ... | ... | ... | ... |`n"
        }

        # Show count above the table when results are truncated.
        $preTableLines = ''
        if ($totalCount -gt $maxDisplay) {
            $preTableLines = "Total open issues: $totalCount`n`n"
        }

        $formatTemplate = @'
 
 
### [Defender XDR > Settings > Identities > Health issues]({0})
 
{1}| Display name | Severity | Type | Affected sensors | Affected domains | Last modified | Status |
| :----------- | :------- | :--- | :--------------- | :--------------- | :------------ | :----- |
{2}
'@

        $mdInfo = $formatTemplate -f $healthPageUrl, $preTableLines, $tableRows
    }

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '41018'
        Title  = 'No open Microsoft Defender for Identity health issues are present in the tenant'
        Status = $passed
        Result = $testResultMarkdown
    }
    Add-ZtTestResultDetail @params
}