Public/Compare-EFEndpointSummary.ps1
|
function Compare-EFEndpointSummary { <# .SYNOPSIS Shows what changed between two EndpointForge computer checks. .DESCRIPTION Compares an earlier check with a later check without reading or changing either computer. Inputs can be EndpointForge endpoint summaries, menu reports, menu sessions, or paths to exported JSON reports. An item is counted as improved only when an earlier problem has a definite passing result in the later check. Missing or unreadable later information is reported as CouldNotCheck and is never described as fixed. .PARAMETER Before The earlier computer check, menu report, menu session, or exported JSON report path. .PARAMETER After The later computer check, menu report, menu session, or exported JSON report path. .PARAMETER AllowDifferentComputer Allows a side-by-side comparison of results from different computers. Without this switch, different computer names are rejected to prevent a misleading progress report. .EXAMPLE Compare-EFEndpointSummary -Before .\before.json -After .\after.json .EXAMPLE Compare-EFEndpointSummary $earlierSummary $latestSummary .OUTPUTS EndpointForge.EndpointComparison #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [Alias('Previous', 'Reference')] [object]$Before, [Parameter(Mandatory, Position = 1)] [Alias('Latest', 'Difference')] [object]$After, [switch]$AllowDifferentComputer ) $beforeSummary = Resolve-EFEndpointSummaryInput -InputObject $Before -Label 'Before' $afterSummary = Resolve-EFEndpointSummaryInput -InputObject $After -Label 'After' $beforeComputer = [string](Get-EFPropertyValue -InputObject $beforeSummary -Name 'ComputerName') $afterComputer = [string](Get-EFPropertyValue -InputObject $afterSummary -Name 'ComputerName') $differentComputer = -not [string]::Equals($beforeComputer.Trim(), $afterComputer.Trim(), [StringComparison]::OrdinalIgnoreCase) if ($differentComputer -and -not $AllowDifferentComputer) { throw [System.ArgumentException]::new( "The checks came from different computers ('$beforeComputer' and '$afterComputer'). Use -AllowDifferentComputer only for an intentional side-by-side comparison." ) } $beforeHealth = Get-EFPropertyValue -InputObject $beforeSummary -Name 'Health' $afterHealth = Get-EFPropertyValue -InputObject $afterSummary -Name 'Health' $beforeChecklist = Get-EFPropertyValue -InputObject $beforeSummary -Name 'Compliance' $afterChecklist = Get-EFPropertyValue -InputObject $afterSummary -Name 'Compliance' $readText = { param([object]$Object, [string]$Name, [string]$FallbackName, [object]$FallbackObject) $value = [string](Get-EFPropertyValue -InputObject $Object -Name $Name) if ([string]::IsNullOrWhiteSpace($value) -and $null -ne $FallbackObject) { $value = [string](Get-EFPropertyValue -InputObject $FallbackObject -Name $FallbackName) } return $value } $beforeChecklistName = & $readText $beforeChecklist 'BaselineName' 'BaselineName' $beforeSummary $afterChecklistName = & $readText $afterChecklist 'BaselineName' 'BaselineName' $afterSummary $beforeChecklistVersion = & $readText $beforeChecklist 'BaselineVersion' 'BaselineVersion' $beforeSummary $afterChecklistVersion = & $readText $afterChecklist 'BaselineVersion' 'BaselineVersion' $afterSummary $checklistChanged = -not [string]::Equals($beforeChecklistName, $afterChecklistName, [StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals($beforeChecklistVersion, $afterChecklistVersion, [StringComparison]::Ordinal) $getHealthTitle = { param([string]$Id) switch -Wildcard ($Id) { 'DiskFreeSpace' { return 'System drive free space' } 'Uptime' { return 'Time since the last Windows restart' } 'PendingReboot' { return 'Windows restart status' } 'Firewall*' { return (($Id -replace '^Firewall', '') + ' firewall') } 'DefenderRealTimeProtection' { return 'Microsoft Defender real-time protection' } 'BitLockerProtection' { return 'BitLocker protection' } 'InventoryCollection' { return 'Computer information collection' } default { return $Id } } } $getState = { param([string]$Source, [string]$Status) if ([string]::IsNullOrWhiteSpace($Status) -or $Status -in @('Unknown', 'Error', 'Incomplete', 'Missing')) { return 'Unknown' } if ($Source -eq 'Health') { if ($Status -eq 'Healthy') { return 'Good' } if ($Status -eq 'NotApplicable') { return 'NotApplicable' } return 'NeedsAttention' } if ($Status -eq 'Compliant') { return 'Good' } if ($Status -eq 'NonCompliant') { return 'NeedsAttention' } if ($Status -eq 'NotApplicable') { return 'NotApplicable' } return 'Unknown' } $makeEvidence = { param([string]$Source, [object]$Item) $idProperty = if ($Source -eq 'Health') { 'Id' } else { 'ControlId' } $id = [string](Get-EFPropertyValue -InputObject $Item -Name $idProperty) if ([string]::IsNullOrWhiteSpace($id)) { throw [System.IO.InvalidDataException]::new("A $Source comparison item does not have an identifier.") } $status = [string](Get-EFPropertyValue -InputObject $Item -Name 'Status') $title = if ($Source -eq 'Health') { & $getHealthTitle $id } else { [string](Get-EFPropertyValue -InputObject $Item -Name 'Title' -Default $id) } [pscustomobject]@{ Key = "${Source}::$id" Source = $Source Id = $id Title = $title Status = $status State = & $getState $Source $status Value = Get-EFPropertyValue -InputObject $Item -Name 'ActualValue' ExpectedValue = if ($Source -eq 'Health') { Get-EFPropertyValue -InputObject $Item -Name 'Threshold' } else { Get-EFPropertyValue -InputObject $Item -Name 'DesiredValue' } Message = [string](Get-EFPropertyValue -InputObject $Item -Name 'Message') } } $beforeItems = @() $afterItems = @() $beforeItems += @((Get-EFPropertyValue -InputObject $beforeHealth -Name 'Checks') | ForEach-Object { & $makeEvidence 'Health' $_ }) $beforeItems += @((Get-EFPropertyValue -InputObject $beforeChecklist -Name 'Results') | ForEach-Object { & $makeEvidence 'Checklist' $_ }) $afterItems += @((Get-EFPropertyValue -InputObject $afterHealth -Name 'Checks') | ForEach-Object { & $makeEvidence 'Health' $_ }) $afterItems += @((Get-EFPropertyValue -InputObject $afterChecklist -Name 'Results') | ForEach-Object { & $makeEvidence 'Checklist' $_ }) $beforeByKey = @{} $afterByKey = @{} foreach ($item in $beforeItems) { if (-not $beforeByKey.ContainsKey($item.Key)) { $beforeByKey[$item.Key] = $item } } foreach ($item in $afterItems) { if (-not $afterByKey.ContainsKey($item.Key)) { $afterByKey[$item.Key] = $item } } $allKeys = @($beforeByKey.Keys + $afterByKey.Keys | Sort-Object -Unique) $changes = @( foreach ($key in $allKeys) { $hasBefore = $beforeByKey.ContainsKey($key) $hasAfter = $afterByKey.ContainsKey($key) $beforeItem = if ($hasBefore) { $beforeByKey[$key] } else { $null } $afterItem = if ($hasAfter) { $afterByKey[$key] } else { $null } $item = if ($hasAfter) { $afterItem } else { $beforeItem } $beforeState = if ($hasBefore) { $beforeItem.State } else { 'Missing' } $afterState = if ($hasAfter) { $afterItem.State } else { 'Missing' } $category = 'Changed' $explanation = '' if ($item.Source -eq 'Checklist' -and $checklistChanged) { $category = 'ChecklistChanged' $explanation = 'The checklist name or version changed. This item is shown for context but is not counted as improved or newly worse.' } elseif (-not $hasAfter) { $category = 'CouldNotCheck' $explanation = 'The later check did not contain this information. Its earlier result has not been treated as fixed.' } elseif ($afterState -eq 'Unknown') { $category = 'CouldNotCheck' $explanation = 'EndpointForge could not get a definite result in the later check. This item has not been treated as fixed.' } elseif (-not $hasBefore) { if ($afterState -eq 'NeedsAttention') { $category = 'NewIssue' $explanation = 'The earlier check did not include this item; the later check found that it needs attention.' } else { $category = 'NowAvailable' $explanation = 'This information was not in the earlier check and is available now.' } } elseif ($beforeState -eq 'Unknown') { if ($afterState -eq 'NeedsAttention') { $category = 'NewIssue' $explanation = 'The earlier check could not determine this result; the later check found that it needs attention.' } else { $category = 'NowAvailable' $explanation = 'The earlier check could not determine this result. The later check can read it now.' } } elseif ($beforeState -eq 'NeedsAttention' -and $afterState -eq 'Good') { $category = 'Improved' $explanation = 'This item needed attention earlier and has a definite passing result now.' } elseif ($beforeState -in @('Good', 'NotApplicable') -and $afterState -eq 'NeedsAttention') { $category = 'NewIssue' $explanation = 'This item did not need attention earlier and does now.' } elseif ($beforeState -eq $afterState) { $category = 'Unchanged' $explanation = if ($afterState -eq 'NeedsAttention') { 'This item still needs attention.' } else { 'This result stayed the same.' } } elseif ($afterState -eq 'NotApplicable') { $category = 'Changed' $explanation = 'The later check says this item is not used on this computer. It has not been treated as fixed.' } else { $category = 'Changed' $explanation = 'The result changed, but the change is not enough to call this item improved or newly worse.' } [pscustomobject]@{ PSTypeName = 'EndpointForge.EndpointComparisonChange' Source = $item.Source Id = $item.Id Title = $item.Title Category = $category BeforeStatus = if ($hasBefore) { $beforeItem.Status } else { 'Not present' } AfterStatus = if ($hasAfter) { $afterItem.Status } else { 'Not present' } BeforeValue = if ($hasBefore) { $beforeItem.Value } else { $null } AfterValue = if ($hasAfter) { $afterItem.Value } else { $null } ExpectedValue = if ($hasAfter) { $afterItem.ExpectedValue } else { $beforeItem.ExpectedValue } BeforeMessage = if ($hasBefore) { $beforeItem.Message } else { '' } AfterMessage = if ($hasAfter) { $afterItem.Message } else { '' } Explanation = $explanation } } ) $improvedCount = @($changes | Where-Object Category -eq 'Improved').Count $newIssueCount = @($changes | Where-Object Category -eq 'NewIssue').Count $couldNotCheckCount = @($changes | Where-Object Category -eq 'CouldNotCheck').Count $unchangedCount = @($changes | Where-Object Category -eq 'Unchanged').Count $nowAvailableCount = @($changes | Where-Object Category -eq 'NowAvailable').Count $otherChangedCount = @($changes | Where-Object Category -in @('Changed', 'ChecklistChanged')).Count $getNumber = { param([object]$Object, [string]$Name) $value = Get-EFPropertyValue -InputObject $Object -Name $Name if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) { return $null } return [math]::Round([double]$value, 1) } $beforeScore = & $getNumber $beforeSummary 'Score' $afterScore = & $getNumber $afterSummary 'Score' $beforeCoverage = & $getNumber $beforeSummary 'CoveragePercent' $afterCoverage = & $getNumber $afterSummary 'CoveragePercent' $scoreChange = if ($null -ne $beforeScore -and $null -ne $afterScore) { [math]::Round($afterScore - $beforeScore, 1) } else { $null } $coverageChange = if ($null -ne $beforeCoverage -and $null -ne $afterCoverage) { [math]::Round($afterCoverage - $beforeCoverage, 1) } else { $null } $summaryText = "$improvedCount improved; $newIssueCount new item(s) need attention; $couldNotCheckCount could not be checked; $unchangedCount stayed the same." if ($checklistChanged) { $summaryText += ' The checklist changed, so this is not a like-for-like progress check.' } if ($differentComputer) { $summaryText += ' The checks came from different computers.' } $nextStep = if ($differentComputer) { 'Use this only as a side-by-side view. Compare two checks from the same computer to measure progress.' } elseif ($checklistChanged) { 'For a reliable progress check, compare results made with the same checklist name and version.' } elseif ($couldNotCheckCount -gt 0) { 'Review the items that could not be checked and try again with Administrator permission if Windows requires it.' } elseif ($newIssueCount -gt 0) { 'Review the new items that need attention before approving any changes.' } elseif ($improvedCount -gt 0) { 'Review the improved items and save this comparison with your support records if needed.' } else { 'No newly improved or newly worse items were found.' } [pscustomobject]@{ PSTypeName = 'EndpointForge.EndpointComparison' ComparedAtUtc = [DateTime]::UtcNow BeforeComputerName = $beforeComputer AfterComputerName = $afterComputer DifferentComputer = $differentComputer BeforeCompletedAtUtc = Get-EFPropertyValue -InputObject $beforeSummary -Name 'CompletedAtUtc' AfterCompletedAtUtc = Get-EFPropertyValue -InputObject $afterSummary -Name 'CompletedAtUtc' BeforeChecklistName = $beforeChecklistName BeforeChecklistVersion = $beforeChecklistVersion AfterChecklistName = $afterChecklistName AfterChecklistVersion = $afterChecklistVersion ChecklistChanged = $checklistChanged IsLikeForLike = -not $differentComputer -and -not $checklistChanged BeforeScore = $beforeScore AfterScore = $afterScore ScoreChange = $scoreChange BeforeCoveragePercent = $beforeCoverage AfterCoveragePercent = $afterCoverage CoverageChange = $coverageChange ImprovedCount = $improvedCount NewCount = $newIssueCount NewIssueCount = $newIssueCount CouldNotCheckCount = $couldNotCheckCount UnchangedCount = $unchangedCount NowAvailableCount = $nowAvailableCount OtherChangedCount = $otherChangedCount AfterCollectionIncomplete = ([string](Get-EFPropertyValue -InputObject $afterSummary -Name 'DataStatus') -ne 'Complete') -or $couldNotCheckCount -gt 0 Changes = @($changes) Summary = $summaryText NextStep = $nextStep } } |