Public/Get-UTCMDrift.ps1

function Get-UTCMDrift {
    <#
    .SYNOPSIS
        Retrieves configuration drifts from the UTCM API.
 
    .DESCRIPTION
        Queries GET /v1.0/admin/configurationManagement/configurationDrifts for
        server-side property-level drift data. Returns granular information about
        which properties drifted, their current vs. desired values, and the drift status.
 
        Active drifts remain available while unresolved. Fixed drifts are retained
        for 30 days after remediation.
 
        This is far more detailed than client-side snapshot comparison — the API provides
        per-property currentValue/desiredValue pairs and active/fixed status tracking.
 
    .PARAMETER MonitorId
        Filter drifts to a specific monitor by its GUID.
 
    .PARAMETER Status
        Filter by drift status: active, fixed.
 
    .PARAMETER ResourceType
        Filter by resource type (e.g., microsoft.exchange.accepteddomain).
 
    .PARAMETER Top
        Limit to the N most recent drifts (client-side).
 
    .PARAMETER IncludeDetails
        Include driftedProperties and resourceInstanceIdentifier (returned only on $select).
 
    .PARAMETER AsJson
        Return results as a JSON string instead of objects.
 
    .OUTPUTS
        PSObject collection representing configurationDrift objects.
 
    .EXAMPLE
        Get-UTCMDrift
 
    .EXAMPLE
        Get-UTCMDrift -Status active -IncludeDetails
 
    .EXAMPLE
        Get-UTCMDrift -MonitorId 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' -AsJson
 
    .EXAMPLE
        Get-UTCMDrift -ResourceType 'microsoft.exchange.accepteddomain' -IncludeDetails
    #>

    [CmdletBinding()]
    param(
        [ValidateScript({
            if (-not (Validate-Guid $_)) { throw "MonitorId '$_' is not a valid GUID." }
            $true
        })]
        [string] $MonitorId,

        [ValidateSet('active','fixed')]
        [string] $Status,

        [string] $ResourceType,

        [int] $Top = 0,

        [switch] $IncludeDetails,

        [switch] $AsJson
    )

    if (Get-Command -Name Ensure-GraphConnection -ErrorAction SilentlyContinue) {
        Ensure-GraphConnection -ReadOnly
    }

    # Build $select
    $selectList = @('id','monitorId','tenantId','resourceType','baselineResourceDisplayName','firstReportedDateTime','status')
    if ($IncludeDetails) {
        $selectList += @('driftedProperties','resourceInstanceIdentifier')
    }

    # Build $filter
    $filters = @()
    if ($MonitorId) {
        $filters += "monitorId eq '$MonitorId'"
    }
    if ($Status) {
        $filters += "status eq '$Status'"
    }
    if ($ResourceType) {
        $escapedResourceType = $ResourceType.Replace("'", "''")
        $filters += "resourceType eq '$escapedResourceType'"
    }

    # Assemble querystring
    $qs = @()
    $qs += "`$select=" + [System.Uri]::EscapeDataString(($selectList -join ','))
    if ($filters.Count -gt 0) {
        $qs += "`$filter=" + [System.Uri]::EscapeDataString(($filters -join ' and '))
    }

    $uri = $script:ConfigurationDriftsUri + '?' + ($qs -join '&')

    # Page through results
    $all = @()
    do {
        $page = Invoke-GraphRequestWithRetry -Method 'GET' -Uri $uri
        if ($page.value) { $all += $page.value }
        $uri = $null
        if ($page -is [System.Collections.IDictionary]) {
            if ($page.ContainsKey('@odata.nextLink')) { $uri = $page['@odata.nextLink'] }
        } elseif ($page.PSObject.Properties.Match('@odata.nextLink').Count) {
            $uri = $page.'@odata.nextLink'
        }
    } while ($uri)

    # Sort by firstReportedDateTime descending
    $sorted = $all | Sort-Object firstReportedDateTime -Descending

    if ($Top -gt 0) {
        $sorted = $sorted | Select-Object -First $Top
    }

    if ($AsJson) {
        return ($sorted | ConvertTo-Json -Depth 20)
    }

    if ($IncludeDetails) {
        return $sorted
    }

    return ($sorted | Select-Object id, monitorId, resourceType, baselineResourceDisplayName, firstReportedDateTime, status)
}