Assets/RemediationLibrary/investigation-common/Compare-Evidence.ps1
|
#requires -Version 5.1 <# .SYNOPSIS Compares two explicitly selected endpoint evidence files without running their contents. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$ReferencePath,[Parameter(Mandatory)][string]$DifferencePath, [ValidateRange(1,720)][int]$MaximumAgeHours=24) $ErrorActionPreference='Stop' function Read-Evidence { param($Path) $file=Get-Item -LiteralPath $Path if($file.Length -gt 10485760){throw 'Evidence exceeds 10 MiB.'} $data=Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json if($data.SchemaVersion -ne '1.0' -or $data.Kind -ne 'IntuneAccess.EndpointEvidence'){throw 'Unsupported evidence schema.'} if(@($data.Facts).Count -gt 1000){throw 'Too many evidence facts.'} $keys=@{} foreach($fact in $data.Facts){if([string]::IsNullOrWhiteSpace($fact.Key) -or $keys.ContainsKey([string]$fact.Key)){throw 'Invalid or duplicate evidence key.'}; if($fact.State -notin @('Observed','Absent','Unavailable')){throw 'Unsupported fact state.'}; $keys[[string]$fact.Key]=$true} return $data } $before=Read-Evidence $ReferencePath; $after=Read-Evidence $DifferencePath if($before.Investigation -ne $after.Investigation){throw 'Choose matching investigation types.'} $warnings=[Collections.Generic.List[string]]::new() $contextMatches=($null -ne $before.Context -and $null -ne $after.Context -and -not [string]::IsNullOrWhiteSpace($before.Context.UserSid) -and $before.Context.UserSid -eq $after.Context.UserSid -and $null -ne $before.Context.IsSystem -and $null -ne $after.Context.IsSystem -and $before.Context.IsSystem -eq $after.Context.IsSystem -and $null -ne $before.Context.Process64Bit -and $null -ne $after.Context.Process64Bit -and $before.Context.Process64Bit -eq $after.Context.Process64Bit) if(-not $contextMatches){$warnings.Add('Execution contexts differ or are incomplete. User-scoped observations are not directly comparable.')} foreach($item in @($before,$after)){ $time=[DateTimeOffset]::MinValue if(-not [DateTimeOffset]::TryParse([string]$item.CollectedAt,[ref]$time)){$warnings.Add('Collection time is missing or invalid.');continue} $age=([DateTimeOffset]::UtcNow-$time.ToUniversalTime()).TotalHours if($age -lt -0.083333){$warnings.Add('Collection time is in the future. Check the endpoint clock.')} elseif($age -gt $MaximumAgeHours){$warnings.Add("Evidence is older than the configured $MaximumAgeHours-hour limit.")} } if((ConvertTo-Json -InputObject $before.Configuration -Depth 15 -Compress) -cne (ConvertTo-Json -InputObject $after.Configuration -Depth 15 -Compress)){$warnings.Add('Collection configurations differ. Compare identical rules before attributing a difference to the endpoint.')} if($before.ComputerName -eq $after.ComputerName){$warnings.Add('Both files report the same computer name. This is not evidence of a separate working reference device.')} $beforeIndex=@{}; $afterIndex=@{} foreach($fact in $before.Facts){$beforeIndex[[string]$fact.Key]=$fact} foreach($fact in $after.Facts){$afterIndex[[string]$fact.Key]=$fact} $rows=@(foreach($key in @(@($beforeIndex.Keys)+@($afterIndex.Keys) | Sort-Object -Unique)){ $left=$beforeIndex[$key]; $right=$afterIndex[$key] $state=if($null -eq $left -or $null -eq $right -or $left.State -eq 'Unavailable' -or $right.State -eq 'Unavailable'){'NotEvaluated'} elseif($left.State -eq $right.State -and (ConvertTo-Json -InputObject $left.Value -Depth 12 -Compress) -ceq (ConvertTo-Json -InputObject $right.Value -Depth 12 -Compress)){'Same'}else{'Different'} $nextCheck=if($state -eq 'NotEvaluated'){'Recollect the missing or unavailable fact in the required context.'} elseif($state -eq 'Same'){'No difference observed for this fact. This does not establish application or device health.'} elseif($key -like 'update/*' -or $key -like 'mdm-update/*'){'Compare the owning Group Policy or CSP and intended update class before changing either device.'} elseif($key -like 'applications/*'){'Confirm product identity, architecture and install scope; do not uninstall based on a display-name difference.'} elseif($key -like 'certificates/*'){'Different device certificates are expected. Compare purpose, issuer, validity, store and application context. Do not copy private keys or delete certificates based on this difference.'} elseif($key -like 'events/*'){'Compare the time window, provider and event ID with the reported incident. Inspect the selected records locally for details; disappearance of an event does not prove a fix.'} else {'Check whether this difference is relevant to the reported symptom. Change one approved variable and recollect both observations.'} [pscustomobject]@{Key=$key;State=$state;Reference=$left;Difference=$right;NextCheck=$nextCheck} }) [pscustomobject]@{SchemaVersion='1.0';Kind='IntuneAccess.EndpointComparison';ReferenceComputer=$before.ComputerName;DifferenceComputer=$after.ComputerName;ReferenceTime=$before.CollectedAt;DifferenceTime=$after.CollectedAt;ContextMatches=$contextMatches;Warnings=$warnings.ToArray();IdentityVerified=$false;DeviceConfigurationChanged=$false;Rows=$rows;Conclusion='Differences are observations, not proven causes or proof that a problem is resolved.'} | ConvertTo-Json -Depth 18 |