Private/Logic/Get-AsrRules.ps1

# Copyright (c) 2026 Sandy Zeng. All rights reserved.
# Source-available. All rights reserved. See LICENSE file.

<#
    Get-AsrRules.ps1 — ASR (Attack Surface Reduction) rule display name resolution helpers.
 
    Author: Sandy Zeng
    Project: IntuneDiff
 
    Version History:
    1.0.0 Initial release.
#>


function Get-AsrRuleDisplayName {
    <#
    .SYNOPSIS
        Returns the friendly display name for an ASR rule setting definition ID.
 
    .DESCRIPTION
        First tries to resolve the name from the settingDefinitions lookup returned by
        Graph. Falls back to a built-in substring map for cases where Graph returns a
        null or missing displayName.
 
    .PARAMETER SettingDefinitionId
        The raw setting definition ID from Graph.
 
    .PARAMETER DefinitionsLookup
        Optional hashtable of settingDefinitionId -> definition object from Graph.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [string]$SettingDefinitionId,
        [hashtable]$DefinitionsLookup
    )

    # Primary: use displayName from the settingDefinitions array returned by Graph.
    # Graph includes child ASR definitions inline, so this works most of the time.
    if ($DefinitionsLookup -and $DefinitionsLookup.ContainsKey($SettingDefinitionId)) {
        $def = $DefinitionsLookup[$SettingDefinitionId]
        if ($def.displayName) { return [string]$def.displayName }
    }

    # Fallback: Graph can return null/missing displayName for ASR child settings depending
    # on the API version or policy type (observed with some Settings Catalog responses).
    # Match on the setting definition ID substring instead — identical approach to v2 backend.
    $id = $SettingDefinitionId.ToLowerInvariant()
    if ($id.Contains('blockexecutionofpotentiallyobfuscatedscripts')) { return 'Block execution of potentially obfuscated scripts' }
    if ($id.Contains('blockwin32apicallsfromofficemacros'))           { return 'Block Win32 API calls from Office macros' }
    if ($id.Contains('blockadobereaderfromcreatingchildprocesses'))   { return 'Block Adobe Reader from creating child processes' }
    if ($id.Contains('blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem')) { return 'Block credential stealing from the Windows local security authority subsystem' }
    if ($id.Contains('blockexecutablefilesrunningunlesstheymeetprevalenceagetrustedlistcriterion')) { return 'Block executable files from running unless they meet a prevalence, age, or trusted list criterion' }
    if ($id.Contains('blockjavascriptorvbscriptfromlaunchingdownloadedexecutablecontent')) { return 'Block JavaScript or VBScript from launching downloaded executable content' }
    if ($id.Contains('blockofficecommunicationappfromcreatingchildprocesses')) { return 'Block Office communication application from creating child processes' }
    if ($id.Contains('blockabuseofexploitedvulnerablesigneddrivers'))  { return 'Block abuse of exploited vulnerable signed drivers (Device)' }
    if ($id.Contains('blockuntrustedunsignedprocessesthatrunfromusb'))  { return 'Block untrusted and unsigned processes that run from USB' }
    if ($id.Contains('blockpersistencethroughwmieventsubscription'))    { return 'Block persistence through WMI event subscription' }
    if ($id.Contains('blockofficeapplicationsfrominjectingcodeintootherprocesses')) { return 'Block Office applications from injecting code into other processes' }
    if ($id.Contains('blockofficeapplicationsfromcreatingexecutablecontent')) { return 'Block Office applications from creating executable content' }

    return $SettingDefinitionId
}

function Get-AsrConfigDisplayName {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [string]$SettingDefinitionId,
        [string]$RawConfigValue,
        [hashtable]$DefinitionsLookup
    )

    if ($DefinitionsLookup -and $DefinitionsLookup.ContainsKey($SettingDefinitionId)) {
        $def = $DefinitionsLookup[$SettingDefinitionId]
        if ($def.options) {
            $opt = @($def.options) | Where-Object {
                $_.itemId -and ($_.itemId.ToString().ToLowerInvariant() -eq $RawConfigValue.ToLowerInvariant())
            } | Select-Object -First 1
            if ($opt) {
                if ($opt.displayName) { return [string]$opt.displayName }
                if ($opt.name)        { return [string]$opt.name }
                return $RawConfigValue
            }
        }
    }

    # Fallback: if no matching option is found in the definition (e.g. definition missing),
    # derive the friendly name from the standard suffix of the raw option ID value.
    $v = $RawConfigValue.ToLowerInvariant()
    if ($v.EndsWith('_block')) { return 'Block' }
    if ($v.EndsWith('_audit')) { return 'Audit' }
    if ($v.EndsWith('_off'))   { return 'Off' }
    if ($v.EndsWith('_warn'))  { return 'Warn' }
    return $RawConfigValue
}

function Get-AsrRulesFromGroupCollection {
    <#
    .SYNOPSIS
        Extracts ASR rules from a Settings Catalog groupSettingCollectionValue.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.IEnumerable])]
    param(
        [object[]]$GroupCollection,
        [object[]]$SettingDefinitions
    )

    $rules = New-Object System.Collections.Generic.List[object]
    if (-not $GroupCollection) { return $rules }

    $lookup = @{}
    foreach ($def in @($SettingDefinitions)) {
        if ($def.id) { $lookup[[string]$def.id] = $def }
    }

    foreach ($group in $GroupCollection) {
        foreach ($child in @($group.children)) {
            $defId = [string]$child.settingDefinitionId
            if (-not $defId -or -not $child.choiceSettingValue) { continue }

            $name = Get-AsrRuleDisplayName -SettingDefinitionId $defId -DefinitionsLookup $lookup
            $raw  = [string]$child.choiceSettingValue.value
            $val  = Get-AsrConfigDisplayName -SettingDefinitionId $defId -RawConfigValue $raw -DefinitionsLookup $lookup

            $rules.Add([pscustomobject]@{
                Id    = $defId
                Name  = "Defender > $name"
                Value = $val
            })

            foreach ($rc in @($child.choiceSettingValue.children)) {
                $childDefId = [string]$rc.settingDefinitionId
                if (-not $childDefId -or -not $childDefId.ToLowerInvariant().Contains('perruleexclusions')) { continue }
                $paths = @($rc.simpleSettingCollectionValue) |
                         ForEach-Object { $_.value } |
                         Where-Object { $_ }
                if ($paths.Count -gt 0) {
                    $rules.Add([pscustomobject]@{
                        Id    = $childDefId
                        Name  = "Defender > $name > Asr Only Per Rule Exclusions"
                        Value = ($paths -join ', ')
                    })
                }
            }
        }
    }

    return $rules
}