maester-tests/Maester/Defender/Test-MtMdiHealthIssues.Tests.ps1

BeforeDiscovery {
    $checkid = "MT.1059"

    try {
        $MdiAllHealthIssues = Invoke-MtGraphRequest -DisableCache -ApiVersion beta -RelativeUri 'security/identities/healthIssues' -OutputType Hashtable -ErrorVariable MdiSecurityApiError
    } catch {
        Write-Verbose "Authentication needed. Please call Connect-MgGraph."
        Add-MtTestResultDetail -SkippedBecause NotConnectedGraph
        return $null
    }

    $MdiHealthIssues = [System.Collections.Generic.List[Object]]::new()

    # Add domainNames and sensorDNSNames as string properties to identify unique health issues
    $MdiAllHealthIssues | ForEach-Object {
        $_ | Add-Member -NotePropertyName 'domainNamesString' -NotePropertyValue ($_.domainNames -join ',') -Force
        $_ | Add-Member -NotePropertyName 'sensorDNSNamesString' -NotePropertyValue ($_.sensorDNSNames -join ',') -Force
    }

    # Get unique health issues (duplicated entries will be created when status of an issue has been changed)
    $textInfo = (Get-Culture).TextInfo
    $MdiAllHealthIssues | Group-Object -Property displayName, domainNamesString, sensorDNSNamesString | ForEach-Object {
        $UniqueHealthIssue = $_.Group | Sort-Object -Property createdDateTime -Descending | Select-Object -First 1
        $UniqueHealthIssue.severity = $textInfo.ToTitleCase($UniqueHealthIssue.severity) # We need title case to be compatible with Maester report
        $UniqueHealthIssue.status = $textInfo.ToTitleCase($UniqueHealthIssue.status) # It just looks better...
        $MdiHealthIssues.Add($UniqueHealthIssue) | Out-Null
    }
    # Group all latest issues based on displayName to group sensors based on particular issue
    $MdiHealthIssuesGrouped = $MdiHealthIssues | Group-Object -Property displayName
}

Describe "Defender for Identity health issues" -Tag "Maester", "Defender", "Security", "MDI", "MT.1059" -ForEach $MdiHealthIssuesGrouped {
    # We need to ID each grouped issue based on it's common displayName, so to keep it consistent and clean, we use MD5
    $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
    $utf8 = New-Object -TypeName System.Text.UTF8Encoding
    $hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($_.Name))).ToLower() -replace '-', ''
    It "MT.1059.$($hash): MDI Health Issues - $($_.Name). See https://maester.dev/docs/tests/MT.1059" -Tag 'MT.1059', "Severity:$($_.Group[0].severity)", $_.Name {
        #region Add detailed test description
        $recommendationSteps = foreach ($recommendationStep in $_.Group[0].recommendations) {
            "$($_.Group[0].recommendations.IndexOf($recommendationStep) + 1). ${recommendationStep}"
        }
        $recommendationSteps = $recommendationSteps -join "`n`n"

        $relatedLinksMd = "* [Microsoft Defender for Identity health issues](https://learn.microsoft.com/en-us/defender-for-identity/health-alerts)", "* [Health issues - Microsoft Defender](https://security.microsoft.com/identities/health-issues)"
        $relatedLinksMd = $relatedLinksMd -join "`n"

        if ($_.Group.additionalInformation) {
            $description = $_.Group[0].description
        }
        $descriptionMd = $_.Name + "`n`n" + $description + "`n`n" + $additionalInformation + "`n`n#### Remediation actions:`n`n" + $recommendationSteps + "`n`n#### Related links:`n`n" + $relatedLinksMd
        #endregion

        #region Add detailed test result
        if ('Open' -in $_.Group.status) {
            $result = $false
            $resultMd = "$($_.Group.status.Where({$_ -eq 'Open'}).count) of $($_.Group.status.count) has issues."
        } else {
            $result = $true
            $resultMd = 'Well done! All issues has been resolved.'
        }
        if ($_.Group.sensorDNSNames -is [System.Collections.IEnumerable]) {
            $resultMdTable += "`n`n#### Sensor DNS names"
            $resultMdTable += "`n`n| Sensor | Status | Created | Last Update |"
            $resultMdTable += "`n| --- | --- | --- | --- |"
            foreach ($issue in $_.Group) {
                if ($issue.status -eq 'Closed') {
                    $issueStatusMd = "✅ $($issue.status)"
                } elseif ($issue.status -eq 'Open') {
                    $issueStatusMd = "❌ $($issue.status)"
                } else {
                    $issueStatusMd = "🗄️ $($issue.status)"
                }
                foreach ($sensorDNSName in $issue.sensorDNSNames) {
                    $resultMdTable += "`n| $($sensorDNSName) | ${issueStatusMd} | $($issue.createdDateTime) | $($issue.lastModifiedDateTime)"
                }
            }
        }
        if ($_.Group.domainNames -is [System.Collections.IEnumerable]) {
            $resultMdTable += "`n`n#### Domain names"
            $resultMdTable += "`n`n| Domain | Status | Created | Last Update |"
            $resultMdTable += "`n| --- | --- | --- | --- |"
            foreach ($issue in $_.Group) {
                if ($issue.status -eq 'Closed') {
                    $issueStatusMd = "✅ $($issue.status)"
                } elseif ($issue.status -eq 'Open') {
                    $issueStatusMd = "❌ $($issue.status)"
                } else {
                    $issueStatusMd = "🗄️ $($issue.status)"
                }
                foreach ($domainName in $issue.domainNames) {
                    $resultMdTable += "`n| $($domainName) | ${issueStatusMd} | $($issue.createdDateTime) | $($issue.lastModifiedDateTime)"
                }
            }
        }
        if ($_.Group.additionalInformation.misconfiguredObjectTypes -is [System.Collections.IEnumerable]) {
            $resultMdTable += "#### Objects"
            $resultMdTable += "`n`n| Object | Status | Permissions | Last Validated |"
            $resultMdTable += "`n| --- | --- | --- | --- |"
            foreach ($issue in $_.Group) {
                if ($issue.status -eq 'Closed') {
                    $issueStatusMd = "✅"
                } elseif ($issue.status -eq 'Open') {
                    $issueStatusMd = "❌"
                } else {
                    $issueStatusMd = "🗄️"
                }
                foreach ($object in $issue.additionalInformation.misconfiguredObjectTypes) {
                    $resultMdTable += "`n| $($object) | ${issueStatusMd} | $($issue.additionalInformation.missingPermissions -join ", ") | $($issue.additionalInformation.validatedOn)"
                }
            }
        }
        $resultMdLink += "`n`n➡️ Open [Health issue - $($_.Name)](https://security.microsoft.com/identities/health-issues) in the Microsoft Defender portal."
        #endregion

        #region Skip if all alerts are dismissed or suppressed
        if (-not ($_.Group.status -notmatch 'Dismissed') -or -not ($_.Group.status -notmatch 'Suppressed')) {
            Add-MtTestResultDetail -Description $descriptionMd -SkippedBecause Custom -SkippedCustomReason "All alerts within this health issue has been **Suppressed** by an administrator.${resultMdTable}`n`nIf this issue is valid for your MDI instance, you can change it's state from **Suppressed** to **Re-open** in the [Microsoft Defender portal](https://security.microsoft.com/identities/health-issues)."
            return $null
        }
        #endregion

        Add-MtTestResultDetail -Description $descriptionMd -Result ($resultMd + $resultMdTable + $resultMdLink) -Severity $_.Group[0].severity

        $result | Should -Be $true -Because $_.Name
    }
}