public/Get-MsecIntuneReusableSetting.ps1

function Get-MsecIntuneReusableSetting {
    <#
    .SYNOPSIS
        Intune reusable settings - the device groups and setting blocks that endpoint security
        policies point at - with how many policies reference each, and what is inside them.

    .DESCRIPTION
        A Device Control policy says "Allow only authorized USBs" and then references a reusable
        setting by GUID. The policy is the rule; the reusable setting is the ANSWER - which USB
        devices, by serial number. Reading the policy alone tells you a decision is being made
        and not what it decides.

        AN UNREFERENCED REUSABLE SETTING IS A LEFTOVER, AND INTUNE DOES NOT CLEAN THEM UP.
        Deleting a policy leaves its reusable settings behind, unreferenced and invisible -
        measured on one tenant, deleting two Device Control policies left two orphans that no
        blade shows as unused. ReferencingPolicyCount is the whole point: zero means nothing
        uses it, and that is a finding rather than a state to tidy away silently.

        THE REFERENCE COUNT AND THE CONTENTS ARE BOTH ABSENT WITHOUT AN EXPLICIT $SELECT. A
        plain GET of this collection returns id, displayName, description, settingDefinitionId
        and lastModifiedDateTime - and silently omits referencingConfigurationPolicyCount and
        settingInstance. Not an error, not an empty value: the properties simply are not there,
        so code that reads them gets $null and reports every setting as unreferenced and empty.
        This command always asks for them.

        ID IS THE JOIN TO THE POLICY. A Device Control policy's rule carries
        `groupid = <guid>`, and that guid is this object's Id. Get-MsecIntuneAsrRule shows the
        raw guid; this is what turns it into a name and a list of devices.

        ENTRIES ARE THE ACCESS-CONTROL LIST. For a Device Control group they are the permitted
        (or denied) devices, each with a friendly name and a serial number or instance path.
        They are projected because an allow-list nobody can read is an allow-list nobody
        reviews - measured on one tenant, 17 entries mixing asset-tagged sticks with 'New test'
        and 'Feng's USB STICK', last edited with no owner and no review date.

    .PARAMETER Name
        Substring match on the display name, case-insensitive.

    .PARAMETER UnreferencedOnly
        Only settings no policy references - the leftovers.

    .EXAMPLE
        Get-MsecIntuneReusableSetting -UnreferencedOnly

        Reusable settings nothing points at. These survive the deletion of the policy that used
        them and are invisible in the portal's policy list.

    .EXAMPLE
        Get-MsecIntuneReusableSetting -Name 'Authorized USBs' | Select-Object -ExpandProperty Entries

        The actual USB allow-list behind "Allow only authorized USBs".

    .EXAMPLE
        Get-MsecIntuneReusableSetting | Format-Table DisplayName, ReferencingPolicyCount, EntryCount, LastModified

        Everything, with how many policies use each and how big it is.

    .OUTPUTS
        PSCustomObject per reusable setting, PSTypeName 'MsecIntuneReusableSetting'.

    .NOTES
        Needs 'DeviceManagementConfiguration.Read.All', which New-MsecApp grants.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string] $Name,

        [switch] $UnreferencedOnly
    )

    Assert-MsecSession

    # EVERY ONE OF THESE IS LOAD-BEARING. Without the $select, referencingConfigurationPolicyCount
    # and settingInstance are absent from the response rather than null, and the command would
    # report every setting as orphaned and empty.
    $select = @(
        'id'
        'displayName'
        'description'
        'settingDefinitionId'
        'lastModifiedDateTime'
        'referencingConfigurationPolicyCount'
        'settingInstance'
    ) -join ','

    try {
        $settings = @(Invoke-MsecGraphRequest -Path "/beta/deviceManagement/reusablePolicySettings?`$select=$select" -All)
    }
    catch {
        if ("$($_.Exception.Message)" -match '403|Forbidden') {
            throw "Forbidden reading reusable policy settings. The msec app needs 'DeviceManagementConfiguration.Read.All'. Re-run New-MsecApp if it is missing. Original error: $($_.Exception.Message)"
        }
        throw
    }

    if (-not $settings.Count) {
        Write-Verbose 'No reusable policy settings exist in this tenant.'
        return
    }

    foreach ($s in $settings) {
        if ($Name -and "$($s.displayName)" -notmatch [regex]::Escape($Name)) { continue }

        # The count is an integer that is legitimately 0. Tested for presence rather than
        # truthiness, so a missing property is reported as $null - unknown - and never as zero,
        # which would read as "orphaned" and could get something deleted that is in use.
        $refCount = if ($null -ne $s.referencingConfigurationPolicyCount) { [int] $s.referencingConfigurationPolicyCount } else { $null }

        if ($UnreferencedOnly -and $refCount -ne 0) { continue }

        # Walk the setting instance for the leaf values that describe each entry. The shape
        # nests differently per setting type, so this collects name/serial/path wherever they
        # appear rather than assuming a fixed depth.
        $names = [System.Collections.Generic.List[string]]::new()
        $ids = [System.Collections.Generic.List[string]]::new()
        $stack = [System.Collections.Generic.Stack[object]]::new()
        if ($s.settingInstance) { $stack.Push($s.settingInstance) }
        while ($stack.Count) {
            $node = $stack.Pop()
            if ($null -eq $node) { continue }
            if ($node -is [System.Collections.IEnumerable] -and $node -isnot [string]) {
                foreach ($i in $node) { $stack.Push($i) }
                continue
            }
            if ($node -isnot [psobject]) { continue }

            $defId = [string] $node.settingDefinitionId
            $value = $node.simpleSettingValue.value
            if ($null -ne $value -and $defId) {
                switch -Wildcard ($defId) {
                    '*_name'            { $names.Add([string] $value); break }
                    '*serialnumberid'   { $ids.Add([string] $value); break }
                    '*instancepathid'   { $ids.Add([string] $value); break }
                    '*deviceid'         { $ids.Add([string] $value); break }
                }
            }
            foreach ($prop in $node.PSObject.Properties) {
                if ($prop.Value -is [psobject] -or ($prop.Value -is [System.Collections.IEnumerable] -and $prop.Value -isnot [string])) {
                    $stack.Push($prop.Value)
                }
            }
        }

        # The kind, from the setting definition. 'policygroups' is a Device Control device
        # group; firewall rule collections and others appear here too.
        $kind = switch -Wildcard ([string] $s.settingDefinitionId) {
            '*devicecontrol_policygroups*' { 'DeviceControlGroup'; break }
            '*firewall*'                   { 'FirewallRule'; break }
            default                        { [string] $s.settingDefinitionId }
        }

        [PSCustomObject]@{
            PSTypeName             = 'MsecIntuneReusableSetting'
            # What a policy's `groupid` points at - the join to Get-MsecIntuneAsrRule's raw output.
            Id                     = [string] $s.id
            DisplayName            = "$($s.displayName)".Trim()
            Description            = [string] $s.description
            Kind                   = $kind
            SettingDefinitionId    = [string] $s.settingDefinitionId
            ReferencingPolicyCount = $refCount
            # $null when the count is unknown, never $true - deleting something on a guess is
            # the one outcome worth designing against here.
            IsUnreferenced         = if ($null -eq $refCount) { $null } else { $refCount -eq 0 }
            EntryCount             = $names.Count
            Entries                = $names.ToArray()
            EntryIdentifiers       = $ids.ToArray()
            LastModified           = if ($s.lastModifiedDateTime) { [datetime] $s.lastModifiedDateTime } else { $null }
            Raw                    = $s
        }
    }
}