tests/Test-Assessment.41050.ps1

<#
.SYNOPSIS
    Attack surface reduction (ASR) rules are enabled in block mode.
 
.DESCRIPTION
    Attack surface reduction rules block high-risk behaviors commonly reused by malware and
    human-operated attacks. This check evaluates the pinned ASR Secure Score control set
    (scid_2500 through scid_2518) by joining control profiles to the latest Microsoft Secure Score
    snapshot and comparing each available score with its maximum score.
 
.NOTES
    Test ID: 41050
    Workshop Task ID: SECOPS-050
    Category: Endpoint threat protection
    Pillar: SecOps
    Required Module: Microsoft.Graph.Authentication
    Required Connection: Microsoft Graph
#>


function Test-Assessment-41050 {
    [ZtTest(
        Category = 'Endpoint threat protection',
        CompatibleLicense = ('WINDEFATP'),
        ImplementationCost = 'Medium',
        Pillar = 'SecOps',
        RiskLevel = 'High',
        Service = ('Graph'),
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce'),
        TestId = 41050,
        Title = 'Attack surface reduction (ASR) rules are enabled in block mode',
        UserImpact = 'Medium'
    )]
    [CmdletBinding()]
    param()

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

    $activity = 'Checking ASR block-mode controls in Microsoft Secure Score'

    $asrControlIds = @(
        'scid_2500', 'scid_2501', 'scid_2502', 'scid_2503', 'scid_2504',
        'scid_2505', 'scid_2506', 'scid_2507', 'scid_2508', 'scid_2509',
        'scid_2510', 'scid_2511', 'scid_2512', 'scid_2513', 'scid_2514',
        'scid_2515', 'scid_2516', 'scid_2517', 'scid_2518'
    )

    $controlProfileError = $null
    $secureScoreError = $null

    # Q1: Read all MDATP Secure Score control profiles, then intersect client-side with pinned IDs.
    Write-ZtProgress -Activity $activity -Status 'Getting MDATP Secure Score control profiles'

    $mdatpControlProfiles = @()
    try {
        $mdatpControlProfiles = @(Invoke-ZtGraphRequest -RelativeUri 'security/secureScoreControlProfiles' -Filter "service eq 'MDATP'" -ApiVersion beta -ErrorAction Stop)
    }
    catch {
        $controlProfileError = $_
        Write-PSFMessage "Failed to retrieve MDATP Secure Score control profiles: $_" -Tag Test -Level Warning
    }

    # Q2: Read the latest Secure Score snapshot; -DisablePaging returns the wrapper object.
    Write-ZtProgress -Activity $activity -Status 'Getting latest Secure Score snapshot'

    $latestSecureScore = $null
    try {
        $secureScoresResponse = Invoke-ZtGraphRequest -RelativeUri 'security/secureScores' -Top 1 -ApiVersion beta -DisablePaging -ErrorAction Stop
        $secureScores = @($secureScoresResponse.value)
        if ($secureScores.Count -gt 0) {
            $latestSecureScore = $secureScores[0]
        }
    }
    catch {
        $secureScoreError = $_
        Write-PSFMessage "Failed to retrieve latest Secure Score snapshot: $_" -Tag Test -Level Warning
    }
    #endregion Data Collection

    #region Assessment Logic
    $passed = $false
    $customStatus = $null

    foreach ($queryError in @($controlProfileError, $secureScoreError) | Where-Object { $null -ne $_ }) {
        if ((Get-ZtHttpStatusCode -ErrorRecord $queryError) -in (401, 403)) {
            $params = @{
                TestId       = '41050'
                Title        = 'Attack surface reduction (ASR) rules are enabled in block mode'
                Status       = $false
                Result       = '⚠️ Microsoft Graph returned HTTP 401 or 403. Verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.'
                CustomStatus = 'Investigate'
            }
            Add-ZtTestResultDetail @params
            return
        }
    }

    $controlProfileById = @{}
    foreach ($controlProfile in @($mdatpControlProfiles | Where-Object { $asrControlIds -contains $_.id })) {
        if ($null -ne $controlProfile.id -and -not $controlProfileById.ContainsKey($controlProfile.id)) {
            $controlProfileById[$controlProfile.id] = $controlProfile
        }
    }

    if ($controlProfileError -or $secureScoreError -or $null -eq $latestSecureScore -or $controlProfileById.Count -eq 0) {
        $params = @{
            TestId       = '41050'
            Title        = 'Attack surface reduction (ASR) rules are enabled in block mode'
            Status       = $false
            Result       = '⚠️ ASR Secure Score data was not found; verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }

    $controlScoreByName = @{}
    foreach ($controlScore in @($latestSecureScore.controlScores)) {
        if ($null -ne $controlScore.controlName -and -not $controlScoreByName.ContainsKey($controlScore.controlName)) {
            $controlScoreByName[$controlScore.controlName] = $controlScore
        }
    }

    $evaluationResults = @()
    foreach ($controlId in $asrControlIds) {
        $controlProfile = if ($controlProfileById.ContainsKey($controlId)) { $controlProfileById[$controlId] } else { $null }
        $matchingScore = if ($controlScoreByName.ContainsKey($controlId)) { $controlScoreByName[$controlId] } else { $null }

        $score = if ($null -ne $matchingScore -and $null -ne $matchingScore.score) { $matchingScore.score } else { $null }
        $maxScore = if ($null -ne $controlProfile -and $null -ne $controlProfile.maxScore) { $controlProfile.maxScore } else { $null }

        $latestStateUpdate = @()
        if ($null -ne $controlProfile) {
            $latestStateUpdate = @($controlProfile.controlStateUpdates | Sort-Object { if ($_.updatedDateTime) { [datetime]$_.updatedDateTime } else { [datetime]::MinValue } } -Descending | Select-Object -First 1)
        }
        $isIgnored = $latestStateUpdate.Count -gt 0 -and $latestStateUpdate[0].state -eq 'ignored'

        $scoreValue = $null
        $scoreIsNumeric = $false
        if ($null -ne $score) {
            try {
                $scoreValue = [double]$score
                $scoreIsNumeric = $true
            }
            catch { }
        }

        $maxScoreValue = $null
        $maxScoreIsNumeric = $false
        if ($null -ne $maxScore) {
            try {
                $maxScoreValue = [double]$maxScore
                $maxScoreIsNumeric = $true
            }
            catch { }
        }

        $status = if ($null -eq $controlProfile) {
            'Investigate'
        }
        elseif ($null -eq $matchingScore) {
            'N/A'
        }
        elseif ($isIgnored) {
            'Skipped'
        }
        elseif (-not $scoreIsNumeric -or -not $maxScoreIsNumeric) {
            'Investigate'
        }
        elseif ($scoreValue -ge $maxScoreValue) {
            'Pass'
        }
        else {
            'Fail'
        }

        $ruleName = if ($null -ne $controlProfile -and -not [string]::IsNullOrWhiteSpace($controlProfile.title)) {
            $controlProfile.title
        }
        else {
            $controlId
        }

        $evaluationResults += [PSCustomObject]@{
            AsrRuleId             = $controlId
            AsrRuleName           = $ruleName
            ActionUrl             = if ($null -ne $controlProfile) { $controlProfile.actionUrl } else { $null }
            Score                 = if ($null -ne $score) { $score } else { 'N/A' }
            MaxScore              = if ($null -ne $maxScore) { $maxScore } else { 'N/A' }
            ImplementationStatus  = if ($null -ne $matchingScore -and -not [string]::IsNullOrWhiteSpace($matchingScore.implementationStatus)) { $matchingScore.implementationStatus } else { 'N/A' }
            LastModifiedDateTime  = if ($null -ne $controlProfile) { $controlProfile.lastModifiedDateTime } else { $null }
            Status                = $status
        }
    }

    $failedItems = @($evaluationResults | Where-Object Status -eq 'Fail')
    $passedItems = @($evaluationResults | Where-Object Status -eq 'Pass')

    if ($failedItems.Count -gt 0) {
        $testResultMarkdown = "❌ One or more attack surface reduction rules are in audit / disabled mode (below their target score).`n`n%TestResult%"
    }
    elseif ($passedItems.Count -gt 0) {
        $passed = $true
        $testResultMarkdown = "✅ All applicable attack surface reduction rules are deployed in block mode.`n`n%TestResult%"
    }
    else {
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ ASR Secure Score data was not found; verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $totalCount = $evaluationResults.Count
    $countLine = "Total ASR controls evaluated: $totalCount"
    $asrPoliciesLink = 'https://intune.microsoft.com/#view/Microsoft_Intune_Workflows/SecurityManagementMenu/~/asr'
    $asrDefenderLink = 'https://security.microsoft.com/asr'

    $portalLinks = "[Microsoft Intune ASR Policies]($asrPoliciesLink) | [Defender XDR > Endpoints > Attack surface reduction]($asrDefenderLink)"

    $tableRows = ''
    foreach ($result in $evaluationResults) {
        $statusDisplay = switch ($result.Status) {
            'Pass' { '✅ Pass' }
            'Fail' { '❌ Fail' }
            'Investigate' { '⚠️ Investigate' }
            'N/A' { 'N/A (no applicable devices)' }
            default { 'Skipped' }
        }
        $lastModified = if ($result.LastModifiedDateTime) { Get-FormattedDate -DateString $result.LastModifiedDateTime } else { 'N/A' }
        $safeRuleName = Get-SafeMarkdown -Text $result.AsrRuleName
        $ruleDisplay = if (-not [string]::IsNullOrWhiteSpace($result.ActionUrl)) {
            "[$safeRuleName]($($result.ActionUrl)) ($($result.AsrRuleId))"
        }
        elseif ($result.AsrRuleName -ne $result.AsrRuleId) {
            "$safeRuleName ($($result.AsrRuleId))"
        }
        else {
            $safeRuleName
        }
        $tableRows += "| $ruleDisplay | $($result.Score) | $($result.MaxScore) | $($result.ImplementationStatus) | $lastModified | $statusDisplay |`n"
    }

    $mdInfo = @"
$countLine
 
$portalLinks
 
| ASR rule (id) | Score | Max score | Implementation status | Last modified | Status |
| :------------ | ----: | --------: | :-------------------- | :------------ | :----- |
$tableRows
"@


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

    $params = @{
        TestId = '41050'
        Title  = 'Attack surface reduction (ASR) rules are enabled in block mode'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}