Public/Test-NSPConditionalAccessBackendParity.ps1
|
function Test-NSPConditionalAccessBackendParity { <# .SYNOPSIS Reads every policy through both the REST transport and the Microsoft.Graph cmdlets and checks their canonical hashes agree. .DESCRIPTION Read-only. Proves the canonical form is transport-independent before the cmdlet path is relied on. Uses the current session (Connect-NSPConditionalAccessGraph). Cmdlet objects are converted to the REST JSON shape with their own ToJsonString() serializer (Microsoft.Graph PowerShell SDK v2). Without it, the conversion falls back to ConvertTo-Json with camelCased keys. One row per policy: Match is $true when both hashes agree. Otherwise Differences lists the canonical fields that disagree. .EXAMPLE Connect-NSPConditionalAccessGraph -Tenant 'contoso.onmicrosoft.com' Test-NSPConditionalAccessBackendParity | Where-Object { -not $_.Match } #> [CmdletBinding()] param( [string]$ConfigRoot ) Import-NSPM365Graph Assert-NSPConditionalAccessSession -ConfigRoot $ConfigRoot | Out-Null if (-not (Get-Command Get-MgIdentityConditionalAccessPolicy -ErrorAction SilentlyContinue)) { Import-Module Microsoft.Graph.Identity.SignIns -ErrorAction Stop } function ConvertTo-CamelCase { param($Value) if ($null -eq $Value) { return $null } if ($Value -is [System.Collections.IDictionary] -or $Value -is [System.Management.Automation.PSCustomObject]) { $out = [ordered]@{} $pairs = if ($Value -is [System.Collections.IDictionary]) { $Value.GetEnumerator() | ForEach-Object { @{ Name = [string]$_.Key; Value = $_.Value } } } else { $Value.PSObject.Properties | ForEach-Object { @{ Name = $_.Name; Value = $_.Value } } } foreach ($pair in $pairs) { if ($pair.Name -eq 'AdditionalProperties') { continue } $name = $pair.Name.Substring(0, 1).ToLowerInvariant() + $pair.Name.Substring(1) $out[$name] = ConvertTo-CamelCase $pair.Value } return $out } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { return , @($Value | ForEach-Object { ConvertTo-CamelCase $_ }) } $Value } function ConvertTo-RestShape { param($CmdletObject) if ($CmdletObject.PSObject.Methods['ToJsonString']) { return $CmdletObject.ToJsonString() | ConvertFrom-Json } ConvertTo-CamelCase (ConvertTo-Json -InputObject $CmdletObject -Depth 32 | ConvertFrom-Json) } function Get-Hashed { param($Policy) $canonical = ConvertTo-NSPConditionalAccessCanonicalPolicy -Policy $Policy [pscustomobject]@{ Canonical = $canonical; Hash = Get-NSPConditionalAccessPolicyHash -Canonical $canonical } } $rest = @{} foreach ($policy in Invoke-NSPGraphCollection -Uri 'identity/conditionalAccess/policies') { $rest[[string](Get-NSPConditionalAccessMapValue $policy 'id')] = @{ Name = Get-NSPConditionalAccessMapValue $policy 'displayName'; Result = Get-Hashed $policy } } $viaCmdlets = @{} foreach ($policy in Get-MgIdentityConditionalAccessPolicy -All -ErrorAction Stop) { $viaCmdlets[[string]$policy.Id] = Get-Hashed (ConvertTo-RestShape $policy) } foreach ($id in @($rest.Keys) + @($viaCmdlets.Keys) | Sort-Object -Unique) { $a = $rest[$id] $b = $viaCmdlets[$id] $differences = if ($a -and $b -and $a.Result.Hash -ne $b.Hash) { @((Compare-NSPConditionalAccessCanonicalPolicy -Live $b.Canonical -Desired $a.Result.Canonical).Differences | ForEach-Object Field) } elseif (-not $a) { @('missing from REST') } elseif (-not $b) { @('missing from cmdlets') } else { @() } [pscustomobject]@{ Id = $id DisplayName = if ($a) { $a.Name } else { $null } Match = [bool]($a -and $b -and $a.Result.Hash -eq $b.Hash) Differences = $differences } } } |