Private/Graph/Get-PolicySettingsBatch.ps1

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

<#
    Get-PolicySettingsBatch.ps1 — Batch-fetches settings for multiple policies using Graph JSON Batching.
 
    Author: Sandy Zeng
    Project: IntuneDiff
 
    Version History:
    1.0.0 Initial release.
#>


function Get-PolicySettingsBatch {
    <#
    .SYNOPSIS
        Fetches settings for multiple policies in parallel using Graph JSON Batching.
 
    .DESCRIPTION
        Accepts an array of policy objects and fetches their settings using the /$batch endpoint.
        Policies are grouped by type and batched in groups of up to 20.
        Intent policies are excluded from batching (they require multi-step calls) and
        fetched sequentially as a fallback.
 
    .PARAMETER Policies
        Array of objects with: Id, TypeKey, Name properties.
 
    .OUTPUTS
        Hashtable keyed by PolicyId. Each value is a hashtable with:
        - settings or settingTemplates (array)
        - name (string)
        - __kind (string)
        Returns $null for a policy if the batch request failed.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Policies
    )

    $results = @{}

    # Deduplicate by Id (same policy can appear multiple times in input)
    $seen = @{}
    $uniquePolicies = @($Policies | Where-Object {
        if ($seen.ContainsKey($_.Id)) { $false } else { $seen[$_.Id] = $true; $true }
    })

    # Separate intent policies (can't be batched due to multi-step dependency)
    $batchable = @($uniquePolicies | Where-Object { $_.TypeKey -ne 'intent' })
    $intents = @($uniquePolicies | Where-Object { $_.TypeKey -eq 'intent' })

    # Build batch requests for batchable policies
    if ($batchable.Count -gt 0) {
        $requests = @($batchable | ForEach-Object {
            $policyId = $_.Id
            $typeKey = $_.TypeKey

            $url = switch ($typeKey) {
                'securityBaseline' {
                    "/deviceManagement/configurationPolicyTemplates('$policyId')/settingTemplates?`$expand=settingDefinitions&`$top=1000"
                }
                default {
                    # settingsCatalog, customSecurityBaseline, endpointSecurity
                    "/deviceManagement/configurationPolicies('$policyId')/settings?`$expand=settingDefinitions&`$top=1000"
                }
            }

            @{
                id     = $policyId
                method = 'GET'
                url    = $url
            }
        })

        $batchResponses = Invoke-GraphBatchRequest -Requests $requests

        # Process responses
        foreach ($p in $batchable) {
            $policyId = $p.Id
            $resp = $batchResponses[$policyId]

            if ($null -eq $resp) {
                # Batch item failed - try individual fallback
                Write-IDLog "Batch fallback for $policyId ($($p.Name))"
                try {
                    $fallback = Get-PolicySettingsData -PolicyId $policyId -PolicyTypeKey $p.TypeKey -PolicyName $p.Name
                    $results[$policyId] = $fallback
                } catch {
                    # Both batch and fallback failed - likely unsupported policy type (certificates, update policies)
                    # Return empty settings instead of null so it's not counted as "failed"
                    Write-IDLog "Skipping unsupported policy $policyId ($($p.Name)): $($_.Exception.Message)"
                    $results[$policyId] = @{
                        id       = $policyId
                        name     = $p.Name
                        settings = @()
                        __kind   = 'skipped'
                    }
                }
                continue
            }

            # Extract the value array from the response (body is a hashtable from -OutputType Hashtable)
            $settingsArray = if ($resp -is [System.Collections.IDictionary]) { $resp['value'] } else { $resp.value }
            if (-not $settingsArray) { $settingsArray = @() }

            if ($p.TypeKey -eq 'securityBaseline') {
                $results[$policyId] = @{
                    id               = $policyId
                    name             = $p.Name
                    settingTemplates = $settingsArray
                    __kind           = 'securityBaseline'
                }
            } else {
                $results[$policyId] = @{
                    id       = $policyId
                    name     = $p.Name
                    settings = $settingsArray
                    __kind   = 'settingsCatalog'
                }
            }
        }
    }

    # Fetch intent policies sequentially (multi-step dependency)
    foreach ($p in $intents) {
        try {
            $results[$p.Id] = Get-PolicySettingsData -PolicyId $p.Id -PolicyTypeKey 'intent' -PolicyName $p.Name
        } catch {
            $results[$p.Id] = $null
        }
    }

    return $results
}