Public/Compare-IntuneAccessSnapshot.ps1
|
function Compare-IntuneAccessSnapshot { <# .SYNOPSIS Compares two local IntuneAccess tenant snapshots. .PARAMETER ReferencePath Earlier snapshot JSON file. .PARAMETER DifferencePath Later snapshot JSON file. .PARAMETER AuditEvent Optional Intune audit events used to correlate changes by exact resource ID. .EXAMPLE Compare-IntuneAccessSnapshot -ReferencePath '.\baseline.snapshot.json' -DifferencePath '.\current.snapshot.json' #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ReferencePath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DifferencePath, [AllowEmptyCollection()] [object[]] $AuditEvent ) $referenceResolved = (Resolve-Path -LiteralPath $ReferencePath -ErrorAction Stop).Path $differenceResolved = (Resolve-Path -LiteralPath $DifferencePath -ErrorAction Stop).Path $reference = Get-Content -LiteralPath $referenceResolved -Raw | ConvertFrom-Json -Depth 50 $difference = Get-Content -LiteralPath $differenceResolved -Raw | ConvertFrom-Json -Depth 50 foreach ($snapshot in @($reference, $difference)) { if ([string] (Get-IntuneAccessProperty $snapshot 'Schema') -ne 'https://controlaltdeletetechbits.github.io/intune-access/schemas/snapshot-1.0.json' -or [string] (Get-IntuneAccessProperty $snapshot 'SchemaVersion') -ne '1.0' -or $null -eq (Get-IntuneAccessProperty $snapshot 'Data')) { throw 'Both files must be IntuneAccess snapshot schema version 1.0.' } $expectedHash = [string] (Get-IntuneAccessProperty $snapshot 'IntegritySha256') $actualHash = Get-IntuneAccessSnapshotHash -Value ((Get-IntuneAccessProperty $snapshot 'Data') | ConvertTo-Json -Depth 40 -Compress) if ($expectedHash -ne $actualHash) { throw 'Snapshot integrity validation failed. The file may have been edited or truncated.' } } if ([string] (Get-IntuneAccessProperty $reference 'IdentityMode') -ne [string] (Get-IntuneAccessProperty $difference 'IdentityMode')) { throw 'Snapshots must use the same identity mode before they can be compared.' } $collectionMap = [ordered] @{ Administrators = 'Administrator' AdminGroups = 'AdminGroup' RoleAssignments = 'RoleAssignment' RoleDefinitions = 'RoleDefinition' ScopeGroups = 'ScopeGroup' ScopeTags = 'ScopeTag' Permissions = 'Permission' Memberships = 'Membership' WorkloadObjects = 'Workload' WorkloadAssignments = 'WorkloadAssignment' WorkloadGroups = 'WorkloadGroup' AssignmentFilters = 'AssignmentFilter' PolicySettings = 'PolicySetting' PolicyConflictFindings = 'PolicyConflictFinding' } $changes = [Collections.Generic.List[object]]::new() foreach ($entry in $collectionMap.GetEnumerator()) { $before = @(Get-IntuneAccessProperty $reference.Data $entry.Key @()) $after = @(Get-IntuneAccessProperty $difference.Data $entry.Key @()) foreach ($change in @(Compare-IntuneAccessSnapshotCollection -Before $before -After $after -EntityType $entry.Value)) { $changes.Add((Add-IntuneAccessSnapshotImpact -Change $change -SnapshotData $difference.Data)) } } if (-not $PSBoundParameters.ContainsKey('AuditEvent')) { $AuditEvent = @(Get-IntuneAccessProperty $difference.Data 'AuditEvents' @()) } foreach ($change in $changes) { $candidateIds = [Collections.Generic.List[string]]::new() foreach ($candidate in @(([string] $change.Id) -split '::|\|')) { if (-not [string]::IsNullOrWhiteSpace($candidate)) { $candidateIds.Add($candidate) } } foreach ($record in @($change.Before, $change.After)) { if ($null -eq $record) { continue } foreach ($propertyName in @('Id', 'WorkloadId', 'FirstPolicyId', 'SecondPolicyId', 'RoleDefinitionId', 'GroupId')) { $candidate = [string] (Get-IntuneAccessProperty $record $propertyName '') if (-not [string]::IsNullOrWhiteSpace($candidate)) { $candidateIds.Add($candidate) } } } $relatedAuditEvents = @($AuditEvent | Where-Object { $resourceIds = @(Get-IntuneAccessProperty $_ 'ResourceIds' @()) @($resourceIds | Where-Object { $_ -in $candidateIds }).Count -gt 0 }) $change | Add-Member -NotePropertyName AuditEvents -NotePropertyValue $relatedAuditEvents -Force $change | Add-Member -NotePropertyName AuditEvidenceState -NotePropertyValue $(if ($relatedAuditEvents.Count) { 'ObservedRelatedEvent' } else { 'NoMatchingEventReturned' }) -Force } $result = [PSCustomObject] @{ PSTypeName = 'IntuneAccess.SnapshotComparison' ReferencePath = $referenceResolved DifferencePath = $differenceResolved ReferenceAt = [DateTimeOffset] $reference.ExportedAt DifferenceAt = [DateTimeOffset] $difference.ExportedAt IdentityMode = [string] $difference.IdentityMode Tenant = $difference.Tenant Changes = @($changes | Sort-Object EntityType, Name, ChangeType) AddedCount = @($changes | Where-Object ChangeType -EQ 'Added').Count RemovedCount = @($changes | Where-Object ChangeType -EQ 'Removed').Count ModifiedCount = @($changes | Where-Object ChangeType -EQ 'Modified').Count BroadImpactCount = @($changes | Where-Object ImpactState -EQ 'BroadTarget').Count ReadOnly = $true GeneratedAt = [DateTimeOffset]::Now ToolVersion = $script:IntuneAccessVersion } $result.PSObject.TypeNames.Insert(0, 'IntuneAccess.SnapshotComparison') $result } |