public/maester/intune/Test-MtIntuneASRRules.ps1

function Test-MtIntuneASRRules {
    <#
    .SYNOPSIS
    Ensure the Microsoft Defender ASR Standard Protection baseline rules are configured in Block or Audit mode.

    .DESCRIPTION
    Checks Intune Endpoint Security Attack Surface Reduction policies (configurationPolicies API) for
    ASR rule configurations.

    ASR rules reduce the attack surface of applications by preventing behaviors commonly abused by malware,
    such as Office macros spawning child processes, credential theft from LSASS, or execution of obfuscated scripts.

    Each ASR rule can be set to one of four modes:
    - Block: Actively prevents the behavior (recommended for production)
    - Audit: Logs the event without blocking (recommended for testing)
    - Warn: Warns the user before allowing the behavior
    - Disabled/Not configured: Rule is inactive

    Pass criteria:
    The test passes if every rule in the Microsoft Defender for Endpoint ASR Standard Protection baseline is
    configured in Block or Audit mode in at least one ASR policy. The Standard Protection baseline is the
    minimum recommended set Microsoft publishes for initial ASR deployment:

    1. Block abuse of exploited vulnerable signed drivers
    2. Block credential stealing from LSASS
    3. Block persistence through WMI event subscription

    See https://learn.microsoft.com/microsoft-365/security/defender-endpoint/attack-surface-reduction-rules-deployment-implement

    Additional ASR rules detected in tenant policies are reported for visibility but do not affect the pass/fail result.

    .EXAMPLE
    Test-MtIntuneASRRules

    Returns true if every Standard Protection baseline rule is configured in Block or Audit mode across the union of all ASR policies in the tenant.

    .LINK
    https://maester.dev/docs/commands/Test-MtIntuneASRRules
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'ASR Rules is the official Microsoft product name')]
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    if (!(Test-MtConnection Graph)) {
        Add-MtTestResultDetail -SkippedBecause NotConnectedGraph
        return $null
    }

    if (-not (Get-MtLicenseInformation -Product Intune)) {
        Add-MtTestResultDetail -SkippedBecause NotLicensedIntune
        return $null
    }

    try {
        Write-Verbose "Querying Intune ASR policies..."
        $asrPolicies = @(Invoke-MtGraphRequest -RelativeUri "deviceManagement/configurationPolicies?`$filter=templateReference/templateFamily eq 'endpointSecurityAttackSurfaceReduction'&`$select=id,name,description,templateReference" -ApiVersion beta)

        Write-Verbose "Found $($asrPolicies.Count) ASR policies."

        if ($asrPolicies.Count -eq 0) {
            $testResultMarkdown = "No Endpoint Security Attack Surface Reduction policies found in Intune.`n`n"
            $testResultMarkdown += "Create an ASR policy under **Endpoint Security > Attack Surface Reduction** with "
            $testResultMarkdown += "ASR rules enabled in **Audit** or **Block** mode to protect against common attack techniques."
            Add-MtTestResultDetail -Result $testResultMarkdown
            return $false
        }

        # Friendly names for ASR rules (extracted from setting definition IDs)
        $asrRuleNames = @{
            'blockexecutionofpotentiallyobfuscatedscripts'                                           = 'Block execution of potentially obfuscated scripts'
            'blockwin32apicallsfromofficemacros'                                                     = 'Block Win32 API calls from Office macros'
            'blockexecutablefilesrunningunlesstheymeetprevalenceagetrustedlistcriterion'              = 'Block executable files unless they meet prevalence/age/trusted list criteria'
            'blockofficecommunicationappfromcreatingchildprocesses'                                   = 'Block Office communication app from creating child processes'
            'blockallofficeapplicationsfromcreatingchildprocesses'                                    = 'Block all Office applications from creating child processes'
            'blockadobereaderfromcreatingchildprocesses'                                              = 'Block Adobe Reader from creating child processes'
            'blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem'                       = 'Block credential stealing from LSASS'
            'blockjavascriptorvbscriptfromlaunchingdownloadedexecutablecontent'                       = 'Block JavaScript/VBScript from launching downloaded executable content'
            'blockuntrustedunsignedprocessesthatrunfromusb'                                           = 'Block untrusted/unsigned processes from USB'
            'blockpersistencethroughwmieventsubscription'                                             = 'Block persistence through WMI event subscription'
            'blockuseofcopiedorimpersonatedsystemtools'                                               = 'Block use of copied or impersonated system tools'
            'blockabuseofexploitedvulnerablesigneddrivers'                                            = 'Block abuse of exploited vulnerable signed drivers'
            'blockprocesscreationsfrompsexecandwmicommands'                                           = 'Block process creations from PSExec and WMI commands'
            'blockofficeapplicationsfromcreatingexecutablecontent'                                    = 'Block Office applications from creating executable content'
            'blockofficeapplicationsfrominjectingcodeintootherprocesses'                              = 'Block Office applications from injecting code into other processes'
            'blockrebootingmachineinsafemode'                                                         = 'Block rebooting machine in Safe Mode'
            'useadvancedprotectionagainstransomware'                                                  = 'Use advanced protection against ransomware'
            'blockexecutablecontentfromemailclientandwebmail'                                         = 'Block executable content from email client and webmail'
            'blockwebshellcreationforservers'                                                         = 'Block webshell creation for servers'
        }

        # Microsoft Standard Protection baseline (minimum recommended ASR rules per Defender deployment guide).
        # Pass requires every rule in this set to be Block or Audit across the union of all ASR policies.
        $standardProtectionRuleSuffixes = @{
            'blockabuseofexploitedvulnerablesigneddrivers'                                            = 'Block abuse of exploited vulnerable signed drivers'
            'blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem'                       = 'Block credential stealing from LSASS'
            'blockpersistencethroughwmieventsubscription'                                             = 'Block persistence through WMI event subscription'
        }

        $policyResults = [System.Collections.Generic.List[hashtable]]::new()
        # Track best mode seen across the union of all policies for each baseline rule.
        # Priority: Block > Audit > Warn > Disabled > Not configured.
        $baselineRuleStatus = @{}
        foreach ($k in $standardProtectionRuleSuffixes.Keys) { $baselineRuleStatus[$k] = 'Not configured' }

        $modeRank = @{ 'Block' = 4; 'Audit' = 3; 'Warn' = 2; 'Disabled' = 1; 'Not configured' = 0 }

        foreach ($policy in $asrPolicies) {
            Write-Verbose "Checking ASR policy: $($policy.name) ($($policy.id))"
            $settingsUri = "deviceManagement/configurationPolicies('$($policy.id)')/settings?`$expand=settingDefinitions&`$top=1000"
            $settingsResponse = @(Invoke-MtGraphRequest -RelativeUri $settingsUri -ApiVersion beta)

            $blockCount = 0
            $auditCount = 0
            $warnCount = 0
            $disabledCount = 0
            $notConfiguredCount = 0
            $ruleDetails = [System.Collections.Generic.List[hashtable]]::new()

            foreach ($setting in $settingsResponse) {
                $defId = $setting.settingInstance.settingDefinitionId
                if ($defId -ne 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules') { continue }

                # ASR rules are stored in a groupSettingCollectionValue
                foreach ($group in $setting.settingInstance.groupSettingCollectionValue) {
                    foreach ($child in $group.children) {
                        $childDefId = $child.settingDefinitionId
                        $val = $child.choiceSettingValue.value

                        # Extract rule name from definition ID
                        $ruleSuffix = $childDefId -replace '^device_vendor_msft_policy_config_defender_attacksurfacereductionrules_', ''
                        $friendlyName = if ($asrRuleNames.ContainsKey($ruleSuffix)) { $asrRuleNames[$ruleSuffix] } else { $ruleSuffix }

                        # Determine enforcement mode from value suffix
                        $mode = 'Not configured'
                        if ($val -like '*_block') { $mode = 'Block'; $blockCount++ }
                        elseif ($val -like '*_audit') { $mode = 'Audit'; $auditCount++ }
                        elseif ($val -like '*_warn') { $mode = 'Warn'; $warnCount++ }
                        elseif ($val -like '*_off') { $mode = 'Disabled'; $disabledCount++ }
                        else { $notConfiguredCount++ }

                        Write-Verbose " Rule: $friendlyName = $mode"
                        $ruleDetails.Add(@{ Name = $friendlyName; Mode = $mode; IsBaseline = $standardProtectionRuleSuffixes.ContainsKey($ruleSuffix) })

                        # Track best mode for baseline rules across all policies
                        if ($standardProtectionRuleSuffixes.ContainsKey($ruleSuffix)) {
                            $current = $baselineRuleStatus[$ruleSuffix]
                            if ($modeRank[$mode] -gt $modeRank[$current]) {
                                $baselineRuleStatus[$ruleSuffix] = $mode
                            }
                        }
                    }
                }
            }

            $policyResults.Add(@{
                Name              = $policy.name
                BlockCount        = $blockCount
                AuditCount        = $auditCount
                WarnCount         = $warnCount
                DisabledCount     = $disabledCount
                NotConfiguredCount = $notConfiguredCount
                TotalRules        = $ruleDetails.Count
                Rules             = $ruleDetails
            })
        }

        # Evaluate baseline coverage across the union of all policies
        $baselineMissing = @()
        foreach ($k in $standardProtectionRuleSuffixes.Keys) {
            $mode = $baselineRuleStatus[$k]
            if ($mode -ne 'Block' -and $mode -ne 'Audit') {
                $baselineMissing += [pscustomobject]@{ Name = $standardProtectionRuleSuffixes[$k]; Mode = $mode }
            }
        }
        $baselinePassed = ($baselineMissing.Count -eq 0)

        # Build result markdown
        $testResultMarkdown = "Found $($asrPolicies.Count) Attack Surface Reduction policy/policies in Intune.`n`n"
        $testResultMarkdown += "**Pass criteria:** Every rule in the Microsoft Defender ASR Standard Protection baseline must be configured in **Block** or **Audit** mode in at least one ASR policy.`n`n"

        $testResultMarkdown += "### Standard Protection baseline coverage (across all policies)`n"
        $testResultMarkdown += "| Baseline rule | Best mode found |`n| --- | --- |`n"
        foreach ($k in $standardProtectionRuleSuffixes.Keys) {
            $testResultMarkdown += "| $($standardProtectionRuleSuffixes[$k]) | $($baselineRuleStatus[$k]) |`n"
        }
        $testResultMarkdown += "`n"

        foreach ($p in $policyResults) {
            $testResultMarkdown += "### $($p.Name)`n"
            $testResultMarkdown += "**$($p.TotalRules) rules:** $($p.BlockCount) Block, $($p.AuditCount) Audit, $($p.WarnCount) Warn, $($p.DisabledCount) Disabled, $($p.NotConfiguredCount) Not configured`n`n"
            $testResultMarkdown += "| Rule | Mode | Baseline |`n| --- | --- | --- |`n"
            foreach ($r in $p.Rules) {
                $baselineMark = if ($r.IsBaseline) { 'Yes' } else { '' }
                $testResultMarkdown += "| $($r.Name) | $($r.Mode) | $baselineMark |`n"
            }
            $testResultMarkdown += "`n"
        }

        if ($baselinePassed) {
            $testResultMarkdown += "**Result:** Well done. Every rule in the Microsoft Defender ASR Standard Protection baseline is configured in **Block** or **Audit** mode."

            # Warn about baseline rules that are still in Audit only across the tenant
            $auditOnly = @($baselineRuleStatus.Keys | Where-Object { $baselineRuleStatus[$_] -eq 'Audit' })
            if ($auditOnly.Count -gt 0) {
                $testResultMarkdown += "`n`n> **Note:** $($auditOnly.Count) baseline rule(s) are only in **Audit** mode. "
                $testResultMarkdown += "Once you have validated impact, transition them to **Block** mode for active protection."
            }

            Add-MtTestResultDetail -Result $testResultMarkdown
            return $true
        } else {
            $missingTable = ($baselineMissing | ForEach-Object { "- $($_.Name) (current: $($_.Mode))" }) -join "`n"
            $testResultMarkdown += "**Result:** The following Standard Protection baseline rules are not in Block or Audit mode:`n`n$missingTable`n`n"
            $testResultMarkdown += "> **Risk:** The Microsoft Defender ASR Standard Protection baseline is the published minimum set of rules required to mitigate "
            $testResultMarkdown += "common credential theft, driver abuse, and persistence techniques. Missing rules leave endpoints exposed to these well-known attack patterns."
            Add-MtTestResultDetail -Result $testResultMarkdown
            return $false
        }
    } catch {
        if ($_.Exception.Response -and $_.Exception.Response.StatusCode -in @(401, 403)) {
            Add-MtTestResultDetail -SkippedBecause NotAuthorized
        } else {
            Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_
        }
        return $null
    }
}