Private/Logic/Compare-AgainstBaseline.ps1
|
# Copyright (c) 2026 Sandy Zeng. All rights reserved. # Source-available. All rights reserved. See LICENSE file. <# Compare-AgainstBaseline.ps1 — Compares a baseline policy against one or more comparison policies. Author: Sandy Zeng Project: IntuneDiff Version History: 1.0.0 Initial release. #> function Compare-AgainstBaseline { <# .SYNOPSIS Compares a baseline policy against one or more comparison policies. .DESCRIPTION PowerShell port of comparisonService.compareAgainstBaseline (core algorithm). Returns a list of result rows with Result = Covered | Missing | Conflict | Extra. .PARAMETER BaselinePolicy Hashtable with .name and .settings (or .settingTemplates) from Graph. .PARAMETER ComparisonPolicies Array of hashtables, each shaped like BaselinePolicy. #> [CmdletBinding()] param( [Parameter(Mandatory)] $BaselinePolicy, [Parameter(Mandatory)] [object[]]$ComparisonPolicies ) $baselineArray = $BaselinePolicy.settings if ($null -eq $baselineArray) { $baselineArray = $BaselinePolicy.settingTemplates } if ($null -eq $baselineArray) { $baselineArray = $BaselinePolicy.value } if ($null -eq $baselineArray) { $baselineArray = @() } $baselineSettings = ConvertTo-ExtractedSettings -SettingsArray $baselineArray $comparisonList = New-Object System.Collections.Generic.List[object] foreach ($p in @($ComparisonPolicies)) { $arr = $p.settings if ($null -eq $arr) { $arr = $p.settingTemplates } if ($null -eq $arr) { $arr = $p.value } if ($null -eq $arr) { $arr = @() } $comparisonList.Add([pscustomobject]@{ Id = $p.id Name = if ($p.name) { $p.name } else { $p.displayName } Settings = ConvertTo-ExtractedSettings -SettingsArray $arr }) } $comparisons = $comparisonList.ToArray() $rows = New-Object System.Collections.Generic.List[object] foreach ($settingId in $baselineSettings.Keys) { $baselineSetting = $baselineSettings[$settingId] $baselineVal = $baselineSetting.Value $baselineHasValue = ($null -ne $baselineVal -and $baselineVal -ne 'Not configured') # Collect every comparison policy that contains this setting $present = New-Object System.Collections.Generic.List[object] foreach ($cp in $comparisons) { if (-not $cp.Settings) { continue } if (-not $cp.Settings.Contains($settingId)) { continue } $cpSetting = $cp.Settings[$settingId] $present.Add([pscustomobject]@{ PolicyName = $cp.Name Value = $cpSetting.Value Raw = $cpSetting.Raw }) } if ($present.Count -eq 0) { # No comparison policy has this setting $cNames = @($comparisons.Name) $cPolicyList = if ($cNames.Count -gt 1) { ($cNames | ForEach-Object { "• $_" }) -join "`n" } else { $cNames[0] } $rows.Add([pscustomobject]@{ SettingName = $baselineSetting.Name SettingId = $settingId Description = $baselineSetting.Description Result = 'missing' PolicyName = $cPolicyList BaselineValue = $baselineVal CurrentValue = $null Category = $baselineSetting.Category BaselineRaw = $baselineSetting.Raw CurrentRaw = $null }) continue } # Group comparison policies by normalized value $byValue = @($present | Group-Object { Get-NormalizedValue $_.Value }) $uniqueValueCount = $byValue.Count if ($uniqueValueCount -gt 1) { # Comparison policies disagree among themselves -> one row per policy foreach ($p in $present) { $rows.Add([pscustomobject]@{ SettingName = $baselineSetting.Name SettingId = $settingId Description = $baselineSetting.Description Result = 'attention' PolicyName = $p.PolicyName BaselineValue = $baselineVal CurrentValue = $p.Value Category = $baselineSetting.Category BaselineRaw = $baselineSetting.Raw CurrentRaw = $p.Raw }) } continue } # All comparison policies that have the setting agree on a single value $currentVal = $present[0].Value $deviceHasValue = ($null -ne $currentVal -and $currentVal -ne 'Not configured') $result = 'covered' if (-not $baselineHasValue -and $deviceHasValue) { $result = 'extra' } elseif ($baselineHasValue -and -not $deviceHasValue) { $result = 'missing' } elseif ((Get-NormalizedValue $baselineVal) -ne (Get-NormalizedValue $currentVal)) { $result = 'conflict' } $sortedNames = @($present.PolicyName | Sort-Object) $policyList = if ($sortedNames.Count -gt 1) { ($sortedNames | ForEach-Object { "• $_" }) -join "`n" } else { $sortedNames[0] } $rows.Add([pscustomobject]@{ SettingName = $baselineSetting.Name SettingId = $settingId Description = $baselineSetting.Description Result = $result PolicyName = $policyList BaselineValue = $baselineVal CurrentValue = $currentVal Category = $baselineSetting.Category BaselineRaw = $baselineSetting.Raw CurrentRaw = $present[0].Raw }) } # Extra settings: present in comparison policies but not in the baseline # Group by SettingId so the same extra setting from N policies collapses to one row. $extraGroups = @{} foreach ($cp in $comparisons) { if (-not $cp.Settings) { continue } foreach ($settingId in $cp.Settings.Keys) { if ($baselineSettings.Contains($settingId)) { continue } if (-not $extraGroups.ContainsKey($settingId)) { $extraGroups[$settingId] = New-Object System.Collections.Generic.List[object] } $cpSetting = $cp.Settings[$settingId] $extraGroups[$settingId].Add([pscustomobject]@{ PolicyName = $cp.Name Value = $cpSetting.Value Raw = $cpSetting.Raw Name = $cpSetting.Name Description= $cpSetting.Description Category = $cpSetting.Category }) } } foreach ($settingId in $extraGroups.Keys) { $entries = $extraGroups[$settingId] $byValue = @($entries | Group-Object { Get-NormalizedValue $_.Value }) if ($byValue.Count -gt 1) { foreach ($e in $entries) { $rows.Add([pscustomobject]@{ SettingName = $e.Name SettingId = $settingId Description = $e.Description Result = 'attention' PolicyName = $e.PolicyName BaselineValue = $null CurrentValue = $e.Value Category = $e.Category BaselineRaw = $null CurrentRaw = $e.Raw }) } } else { $rows.Add([pscustomobject]@{ SettingName = $entries[0].Name SettingId = $settingId Description = $entries[0].Description Result = 'extra' PolicyName = $( $enames = @($entries.PolicyName | Sort-Object); if ($enames.Count -gt 1) { ($enames | ForEach-Object { "• $_" }) -join "`n" } else { $enames[0] } ) BaselineValue = $null CurrentValue = $entries[0].Value Category = $entries[0].Category BaselineRaw = $null CurrentRaw = $entries[0].Raw }) } } # Sort: covered, conflict, missing, extra, attention; then by setting name $order = @{ 'covered' = 0; 'conflict' = 1; 'missing' = 2; 'extra' = 3; 'attention' = 4 } $sorted = $rows | Sort-Object @{Expression = { $order[$_.Result] }}, SettingName return [pscustomobject]@{ BaselineName = if ($BaselinePolicy.name) { $BaselinePolicy.name } else { $BaselinePolicy.displayName } ComparisonPolicies = $comparisons | Select-Object Id, Name Rows = @($sorted) Summary = [pscustomobject]@{ TotalBaselineSettings = $baselineSettings.Keys.Count CoveredSettings = @($sorted | Where-Object Result -eq 'covered').Count MissingSettings = @($sorted | Where-Object Result -eq 'missing').Count ConflictSettings = @($sorted | Where-Object Result -eq 'conflict').Count AttentionSettings = @($sorted | Where-Object Result -eq 'attention').Count ExtraSettings = @($sorted | Where-Object Result -eq 'extra').Count } } } function Get-NormalizedValue { <# .SYNOPSIS Normalises a setting value to a trimmed string for comparison. Returns empty string for null. #> [CmdletBinding()] param([AllowNull()] $Value) if ($null -eq $Value) { return '' } if ($Value -is [string]) { return $Value.Trim() } return [string]$Value } |