tests/Test-Assessment.41009.ps1

<#
.SYNOPSIS
    Dormant accounts have been removed from sensitive Active Directory groups.
 
.DESCRIPTION
    Validates the Microsoft Defender for Identity "Remove dormant accounts from sensitive
    groups" posture recommendation via Microsoft Secure Score.
 
    Accounts that have not authenticated for 180+ days while retaining membership in
    sensitive groups such as Domain Admins or Enterprise Admins are high-value targets:
    their owners are not monitoring sign-ins, passwords are rarely rotated, and any
    pre-existing credential exposure remains exploitable indefinitely.
 
    The check reads the Secure Score control profile for AATP_DormantAccounts and the
    latest per-control score snapshot, then returns:
      Pass – MDI reports no dormant accounts in sensitive groups.
      Fail – One or more dormant accounts remain in sensitive groups.
      Investigate – The MDI posture control is not present in this tenant's Secure Score
                    (MDI not onboarded, sensors not healthy, or not yet provisioned).
 
.NOTES
    Test ID: 41009
    Workshop Task: SECOPS-009
    Pillar: SecOps
    Category: Identity threat protection
    Risk Level: High
    Supported Clouds: Global, USGov, USGovDoD
    Required Permission: SecurityEvents.Read.All (Application or Delegated)
#>


function Test-Assessment-41009 {
    [ZtTest(
        Category = 'Identity threat protection',
        ImplementationCost = 'Low',
        CompatibleLicense = ('ATA'),
        Service = ('Graph'),
        Pillar = 'SecOps',
        RiskLevel = 'High',
        SfiPillar = 'Protect identities and secrets',
        TenantType = ('Workforce'),
        TestId = 41009,
        Title = 'Dormant accounts have been removed from sensitive Active Directory groups',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection

    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity    = 'Checking dormant accounts in sensitive Active Directory groups via MDI Secure Score'
    Write-ZtProgress -Activity $activity -Status 'Retrieving MDI secure score control profile'
       
    # Q1 – Retrieve the MDI "Remove dormant accounts from sensitive groups" control profile.
    # This uses a $filter query; an empty result set indicates the profile is absent from this tenant.
    
    $controlProfile = $null
    $errorMsgQ1     = $null
    $httpStatusQ1   = $null

    try {

        $profileResults = Invoke-ZtGraphRequest -RelativeUri 'security/secureScoreControlProfiles' -Filter "service eq 'Azure ATP' and id eq 'AATP_DormantAccounts'" -ApiVersion beta -ErrorAction Stop
        $controlProfile = $profileResults | Select-Object -First 1
    }
    catch {
        $errorMsgQ1   = $_
        $httpStatusQ1 = Get-ZtHttpStatusCode -ErrorRecord $_
        Write-PSFMessage "Failed to retrieve MDI control profile: $errorMsgQ1" -Level Warning
    }

    # Q2 – Retrieve the most recent Secure Score snapshot.
    $latestSecureScore = $null

    if ($null -ne $controlProfile) {
        Write-ZtProgress -Activity $activity -Status 'Retrieving latest Microsoft Secure Score'
        try {
            $scoreResponse     = Invoke-ZtGraphRequest -RelativeUri 'security/secureScores' -Top 1 -ApiVersion beta -DisablePaging -ErrorAction Stop
            $latestSecureScore = $scoreResponse.value | Select-Object -First 1
        }
        catch {
            Write-PSFMessage "Failed to retrieve Secure Score: $_" -Level Warning
        }
    }

    #endregion Data Collection

    #region Assessment Logic

    $passed       = $false
    $customStatus = $null

    # ── Investigate: Q1 returned no profile (MDI not onboarded or not yet provisioned) or 401/403 (insufficient permissions) ──
    if ($null -eq $controlProfile) {
        if ($httpStatusQ1 -in @(401, 403)) {
            $investigateReason = 'The **SecurityEvents.Read.All** permission is required to read Secure Score control profiles. Verify the permission is consented and re-run the assessment.'
        }
        elseif ($null -ne $errorMsgQ1) {
            $investigateReason = "Microsoft Graph returned an unexpected error retrieving the MDI Secure Score control profile. Re-run the assessment in 5–10 minutes and open a support ticket if the error persists."
        }
        else {
            $investigateReason = "The Microsoft Defender for Identity posture recommendation `"Remove dormant accounts from sensitive groups`" was not found in the tenant's Microsoft Secure Score; verify that MDI posture assessments are enabled."
        }

        $testResultMarkdown = "⚠️ $investigateReason"
        $customStatus       = 'Investigate'

        $params = @{
            TestId       = '41009'
            Title        = 'Dormant accounts have been removed from sensitive Active Directory groups'
            Status       = $passed
            Result       = $testResultMarkdown
            CustomStatus = $customStatus
        }
        Add-ZtTestResultDetail @params
        return
    }

    # Resolve profile fields
    $controlId     = $controlProfile.id
    $profileTitle  = $controlProfile.title
    $maxScore      = $controlProfile.maxScore
    $actionUrl     = $controlProfile.actionUrl

    # ── Investigate: Q2 returned no data at all ──
    if ($null -eq $latestSecureScore) {
        $testResultMarkdown = "⚠️ The MDI dormant-accounts control profile exists but the current Microsoft Secure Score snapshot could not be retrieved."
        $customStatus       = 'Investigate'

        $params = @{
            TestId       = '41009'
            Title        = 'Dormant accounts have been removed from sensitive Active Directory groups'
            Status       = $passed
            Result       = $testResultMarkdown
            CustomStatus = $customStatus
        }
        Add-ZtTestResultDetail @params
        return
    }

    # ── Locate the per-control entry inside controlScores[] ──
    $controlScoreEntry = $null
    if ($latestSecureScore.controlScores) {
        $controlScoreEntry = $latestSecureScore.controlScores |
            Where-Object { $_.controlName -eq $controlId } |
            Select-Object -First 1
    }

    # ── Investigate: profile exists but the snapshot has no entry for this control ──
    if ($null -eq $controlScoreEntry) {
        $testResultMarkdown = "⚠️ The MDI dormant-accounts control profile ($controlId) exists but the latest Secure Score snapshot has no scored entry for this control."
        $customStatus       = 'Investigate'

        $params = @{
            TestId       = '41009'
            Title        = 'Dormant accounts have been removed from sensitive Active Directory groups'
            Status       = $passed
            Result       = $testResultMarkdown
            CustomStatus = $customStatus
        }
        Add-ZtTestResultDetail @params
        return
    }

    # ── Evaluate Pass / Fail ──
    $currentScore         = $controlScoreEntry.score
    $scoreInPercentage    = $controlScoreEntry.scoreInPercentage
    $implementationStatus = $controlScoreEntry.implementationStatus
    $lastSynced           = $controlScoreEntry.lastSynced

    if ($currentScore -eq $maxScore) {
        $passed             = $true
        $testResultMarkdown = "✅ No dormant accounts are members of sensitive Active Directory groups.`n`n%TestResult%"
    }
    else {
        $passed             = $false
        $testResultMarkdown = "❌ One or more dormant accounts are members of sensitive Active Directory groups.`n`n%TestResult%"
    }

    #endregion Assessment Logic

    #region Report Generation

    $defenderLink = 'https://security.microsoft.com/securescore?viewid=actions'

    $scoreDisplay     = if ($null -ne $currentScore) { $currentScore } else { '-' }
    $maxDisplay       = if ($null -ne $maxScore) { $maxScore } else { '-' }
    $pctDisplay       = if ($null -ne $scoreInPercentage) { "$([math]::Round($scoreInPercentage, 1))%" } else { '-' }
    $impStatusDisplay = if (-not [string]::IsNullOrEmpty($implementationStatus)) { $implementationStatus } else { '-' }
    $syncDisplay      = if (-not [string]::IsNullOrEmpty($lastSynced)) { Get-FormattedDate -DateString $lastSynced } else { '-' }
    $statusLabel      = if ($passed) { '✅ Pass' } else { '❌ Fail' }

    $actionLinkMarkdown = ''
    if (-not [string]::IsNullOrWhiteSpace($actionUrl)) {
        $actionLinkMarkdown = "[$(Get-SafeMarkdown $profileTitle)]($actionUrl)"
    }
    elseif (-not [string]::IsNullOrWhiteSpace($profileTitle)) {
        $actionLinkMarkdown = Get-SafeMarkdown $profileTitle
    }
    else {
        $actionLinkMarkdown = 'Remove dormant accounts from sensitive groups'
    }
    
    $tableRows = "| $actionLinkMarkdown | $scoreDisplay | $maxDisplay | $pctDisplay | $impStatusDisplay | $syncDisplay | $statusLabel |`n"

    $mdFailLink = ''
    if (-not $passed) {
        $mdFailLink = "`n## [Defender XDR > Secure Score > Recommendations]($defenderLink)`n"
    }

    $mdTable = @"
 
$mdFailLink
| Recommendation title | Current score | Maximum score | Score % | Implementation status | Last synced | Status |
| :------------------- | :-----------: | :-----------: | :-----: | :-------------------- | :---------- | :----: |
$tableRows
"@


    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdTable

    #endregion Report Generation

    $params = @{
        TestId = '41009'
        Title  = 'Dormant accounts have been removed from sensitive Active Directory groups'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}