Private/ConvertTo-AACConfigurationSnapshot.ps1

function ConvertTo-AACConfigurationSnapshot {
    <#
    .SYNOPSIS
        Turns resources (Resource Graph rows) into a configuration snapshot:
        each resource's settings flattened to 'path = value' - SKU, kind,
        location, tags and properties - without what changes on its own
        (provisioning states, timestamps, counters, status), so two
        snapshots differ only where the configuration does.
    .DESCRIPTION
        Paths are dotted, as Resource Graph's change history writes them:
        'properties.minimumTlsVersion', 'sku.name', 'tags.CostCenter'.
        Arrays are kept whole, as compact JSON. Returns a hashtable:
        resource ID (lower case) -> @{ Type; Name; Settings (path -> text) }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [AllowEmptyCollection()]
        [object[]] $Resource = @()
    )

    $volatile = '(?i)(provisioningstate|creationtime|createdat|createdon|timecreated|lastmodified|changedtime|modifiedtime|timestamp|lastupdated|\.etag$|statusof|\.status$|\.state$|instanceview|\.extended\.|statistics|usage|count$|\.resourceguid$|uniqueid|\.version$|powerstate|\.lastsync|primaryendpoints|secondaryendpoints|\.id$)'
    $flatten = {
        param($Value, [string] $Path, [hashtable] $Into)
        if ($Path -and $Path -match $volatile -and $Path -notmatch '(?i)^(sku|kind|location)$') { return }
        if ($Value -is [System.Collections.IDictionary]) {
            foreach ($k in @($Value.Keys | Sort-Object)) { & $flatten $Value[$k] $(if ($Path) { "$Path.$k" } else { [string]$k }) $Into }
        }
        elseif ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
            $Into[$Path] = ConvertTo-Json -InputObject @($Value) -Depth 20 -Compress
        }
        elseif ($null -ne $Value) { $Into[$Path] = $(if ($Value -is [bool]) { $Value.ToString().ToLowerInvariant() } else { [string]$Value }) }
    }
    $snapshot = @{}
    foreach ($r in $Resource) {
        $get = { param([string] $Name) if ($r -is [System.Collections.IDictionary]) { $r[$Name] } else { $p = $r.PSObject.Properties[$Name]; if ($p) { $p.Value } } }
        $values = @{}
        foreach ($part in 'sku', 'kind', 'location', 'tags', 'identity', 'properties') {
            $v = & $get $part
            if ($null -eq $v) { continue }
            if ($part -eq 'identity' -and $v -is [System.Collections.IDictionary]) { $values['identity.type'] = [string]$v['type']; continue }
            & $flatten $v $part $values
        }
        $snapshot[([string](& $get 'id')).ToLowerInvariant()] = @{ Type = ([string](& $get 'type')).ToLowerInvariant(); Name = [string](& $get 'name'); Settings = $values }
    }
    $snapshot
}