modules/shared/Get-NewCriticalFindings.ps1
|
#Requires -Version 7.4 <# .SYNOPSIS Identify net-new and escalated Critical findings from a Compare-EntitySnapshots drift result. .DESCRIPTION Given a drift hashtable produced by Compare-EntitySnapshots, returns an array of PSCustomObjects describing Critical findings that are genuinely new: - 'New' -- entity appears in Added with one or more Critical observations. - 'Escalated' -- entity appears in Modified and the Critical observation count increased compared to the previous snapshot. Unchanged entities and modifications that did not increase the Critical count are excluded, suppressing noise from standing (known) findings. Also exports New-NoBaselineDriftResult, a helper that synthesises an all-Added drift result from a single entities.json path. Use this for first-run mode where no previous snapshot exists and every Critical observation should be treated as new. #> Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Get-NewCriticalFindings { <# .SYNOPSIS Returns net-new and escalated Critical findings from a Compare-EntitySnapshots drift result. .OUTPUTS PSCustomObject[] Each element has: EntityId, EntityType, ChangeKind (New|Escalated), CriticalObservationCount, Titles (string[]). #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [Parameter(Mandatory)] [object] $DriftResult ) $results = [System.Collections.Generic.List[object]]::new() # New entities with at least one Critical observation foreach ($entry in @($DriftResult.Added)) { if (-not $entry -or -not $entry.Current) { continue } $critObs = @($entry.Current.Observations | Where-Object { $_ -and $_.Severity -and ([string]$_.Severity).ToLowerInvariant() -eq 'critical' }) if ($critObs.Count -eq 0) { continue } $results.Add([pscustomobject]@{ EntityId = [string]$entry.EntityId EntityType = if ($entry.EntityType) { [string]$entry.EntityType } else { '' } ChangeKind = 'New' CriticalObservationCount = $critObs.Count Titles = @($critObs | ForEach-Object { if ($_.PSObject.Properties['Title'] -and $_.Title) { [string]$_.Title } else { '' } }) }) | Out-Null } # Modified entities where the Critical observation count increased (severity escalation) foreach ($entry in @($DriftResult.Modified)) { if (-not $entry) { continue } $prevCritCount = 0 if ($entry.PSObject.Properties['Previous'] -and $entry.Previous -and $entry.Previous.PSObject.Properties['Observations']) { $prevCritCount = @($entry.Previous.Observations | Where-Object { $_ -and $_.Severity -and ([string]$_.Severity).ToLowerInvariant() -eq 'critical' }).Count } $currCritObs = @() if ($entry.PSObject.Properties['Current'] -and $entry.Current -and $entry.Current.PSObject.Properties['Observations']) { $currCritObs = @($entry.Current.Observations | Where-Object { $_ -and $_.Severity -and ([string]$_.Severity).ToLowerInvariant() -eq 'critical' }) } $currCritCount = $currCritObs.Count if ($currCritCount -le $prevCritCount) { continue } $results.Add([pscustomobject]@{ EntityId = [string]$entry.EntityId EntityType = if ($entry.EntityType) { [string]$entry.EntityType } else { '' } ChangeKind = 'Escalated' CriticalObservationCount = $currCritCount - $prevCritCount Titles = @($currCritObs | ForEach-Object { if ($_.PSObject.Properties['Title'] -and $_.Title) { [string]$_.Title } else { '' } }) }) | Out-Null } return , $results.ToArray() } function New-NoBaselineDriftResult { <# .SYNOPSIS Creates an all-Added drift result from entities.json for first-run (no baseline) mode. .DESCRIPTION Reads the entities.json at the given path via Get-EntitySnapshotPayload and wraps every entity as an 'Added' drift entry. Pass the result to Get-NewCriticalFindings to treat every Critical observation as net-new. .PARAMETER EntitiesPath Absolute path to the current run's entities.json. #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory)] [string] $EntitiesPath ) # Get-EntitySnapshotPayload is defined in Compare-EntitySnapshots.ps1; require it if (-not (Get-Command Get-EntitySnapshotPayload -ErrorAction SilentlyContinue)) { throw 'Get-EntitySnapshotPayload not found -- dot-source Compare-EntitySnapshots.ps1 first.' } $payload = Get-EntitySnapshotPayload -Path $EntitiesPath $added = [System.Collections.Generic.List[object]]::new() foreach ($entity in @($payload.Entities)) { if (-not $entity) { continue } $entityId = if ($entity.PSObject.Properties['EntityId']) { [string]$entity.EntityId } else { '' } $entityType = if ($entity.PSObject.Properties['EntityType']) { $entity.EntityType } else { $null } $platform = if ($entity.PSObject.Properties['Platform']) { $entity.Platform } else { $null } if (-not $entityId) { continue } $added.Add([pscustomobject]@{ ChangeKind = 'Added' EntityId = $entityId EntityType = $entityType Platform = $platform Severity = 'Info' Previous = $null Current = $entity ChangedPaths = @() }) | Out-Null } return [ordered]@{ Added = @($added) Removed = @() Modified = @() Unchanged = @() } } |