Private/Graph/Get-DeviceSettingStatusData.ps1

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

<#
    Get-DeviceSettingStatusData.ps1 — Retrieves per-setting status for a policy on a device.
 
    Author: Sandy Zeng
    Project: IntuneDiff
 
    Version History:
    1.0.0 Initial release.
#>


function Get-DeviceSettingStatusData {
    <#
    .SYNOPSIS
        Returns per-setting status for a single policy applied to a device.
 
    .DESCRIPTION
        For a policy whose overall status on a device is Conflict or Error, the
        policy-level status does not tell which individual settings actually failed.
        This function calls reports/getConfigurationSettingsReport (paginated via
        skip/top) to obtain the status of every setting for the given policy/device/user.
 
        Returns an ordered hashtable keyed by a normalized setting name whose values are
        hashtables: @{ StatusText; SettingStatus; ErrorCode; SettingId; SettingInstanceId }.
 
    .PARAMETER PolicyId
        The configuration policy id to query.
 
    .PARAMETER DeviceId
        The Intune device id (GUID).
 
    .PARAMETER UserId
        The user id (GUID) from step-1 policy assignment data. For system-account
        rows, pass 00000000-0000-0000-0000-000000000000.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(Mandatory)]
        [string]$PolicyId,

        [Parameter(Mandatory)]
        [string]$DeviceId,

        [Parameter(Mandatory)]
        [string]$UserId
    )

    if ($DeviceId -notmatch '^[0-9a-fA-F\-]{36}$') {
        throw "Invalid DeviceId format: '$DeviceId'. Expected a GUID."
    }

    $statusMap = @{ 0 = 'Succeeded'; 1 = 'Not applicable'; 2 = 'Succeeded'; 5 = 'Error'; 6 = 'Conflict' }

    if ([string]::IsNullOrWhiteSpace($UserId)) {
        throw "UserId is required for getConfigurationSettingsReport filtering."
    }

    $filter = "(PolicyId eq '$PolicyId') and (DeviceId eq '$DeviceId') and (UserId eq '$UserId')"

    $result = [ordered]@{}

    $pageSize = 50
    $skip = 0
    $totalRowCount = 0
    $schema = @()
    $allValues = New-Object System.Collections.Generic.List[object]

    while ($true) {
        $body = @{
            select = @('SettingName', 'SettingStatus', 'ErrorCode', 'SettingId', 'SettingInstanceId')
            filter = $filter
            skip   = $skip
            search = ''
            top    = $pageSize
        }

        $response = Invoke-IntuneDiffRequest -Method POST -Uri '/beta/deviceManagement/reports/getConfigurationSettingsReport' -Body $body
        if ($response -is [string]) { $response = $response | ConvertFrom-Json }

        if ($schema.Count -eq 0 -and $response.Schema) { $schema = $response.Schema }

        $pageValues = $response.Values
        if (-not $pageValues) { $pageValues = @() }
        if ($response.TotalRowCount) { $totalRowCount = [int]$response.TotalRowCount }

        if ($pageValues.Count -eq 0) { break }

        foreach ($row in $pageValues) { $allValues.Add($row) }
        $skip += $pageSize

        if ($totalRowCount -gt 0 -and $allValues.Count -ge $totalRowCount) { break }
        if ($pageValues.Count -lt $pageSize) { break }
    }

    foreach ($valueArray in $allValues) {
        $row = @($valueArray)
        $obj = @{}
        for ($i = 0; $i -lt $schema.Count; $i++) {
            $columnName = $schema[$i].Column
            $obj[$columnName] = $row[$i]
        }

        $settingName = [string]$obj['SettingName']
        if (-not $settingName) { continue }

        $statusInt = if ($null -ne $obj['SettingStatus']) { [int]$obj['SettingStatus'] } else { 2 }
        $statusText = if ($statusMap.ContainsKey($statusInt)) { $statusMap[$statusInt] } else { "Status $statusInt" }

        $key = ConvertTo-NormalizedSettingName -Name $settingName
        $result[$key] = @{
            StatusText        = $statusText
            SettingStatus     = $statusInt
            ErrorCode         = $obj['ErrorCode']
            SettingId         = [string]$obj['SettingId']
            SettingInstanceId = [string]$obj['SettingInstanceId']
        }
    }

    return $result
}

function ConvertTo-NormalizedSettingName {
    <#
    .SYNOPSIS
        Normalizes a setting display name for matching report data against extracted settings.
 
    .DESCRIPTION
        The per-setting reports return the leaf setting name, while extracted settings use a
        full category > setting path. This helper reduces both to a comparable form: the leaf
        segment (text after the last ' > '), with a trailing '(Device)'/'(User)' scope suffix
        removed, trimmed and lower-cased.
 
    .PARAMETER Name
        The setting display name (leaf or full path).
    #>

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

    if (-not $Name) { return '' }

    $leaf = $Name
    $sepIndex = $leaf.LastIndexOf(' > ')
    if ($sepIndex -ge 0) { $leaf = $leaf.Substring($sepIndex + 3) }

    $leaf = $leaf -replace '\s*\((Device|User)\)\s*$', ''

    return $leaf.Trim().ToLowerInvariant()
}