Public/Compare-CaBaseline.ps1

function Compare-CaBaseline {
    <#
    .EXTERNALHELP CaOutcome-Help.xml
    .SYNOPSIS
        Compares a fresh set of outcomes against a recorded baseline and reports what moved
    #>


    [CmdletBinding(DefaultParameterSetName = 'Object')]
    [OutputType('CaOutcome.Drift')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [object[]]$Outcome,

        [Parameter(Mandatory, ParameterSetName = 'Object')]
        [ValidateNotNull()]
        [object]$Baseline,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            if (-not (Test-Path -LiteralPath $Path)) {
                throw "No baseline at '$Path'."
            }
            $Baseline = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
        }

        $stored = Get-CaProperty -InputObject $Baseline -Name 'scenarios'
        if ($null -eq $stored) {
            throw 'The baseline has no scenarios property. It may be from a different tool.'
        }

        $storedNames = @()
        if ($stored -is [System.Collections.IDictionary]) {
            $storedNames = @($stored.Keys)
        } elseif ($stored.PSObject) {
            $storedNames = @($stored.PSObject.Properties | Select-Object -ExpandProperty Name)
        }

        $seen = New-Object System.Collections.Generic.List[string]
        $results = New-Object System.Collections.Generic.List[object]
    }

    process {
        foreach ($item in $Outcome) {
            if ($null -eq $item) { continue }

            $name = [string]$item.Scenario
            $seen.Add($name)

            if ($item.PSObject.Properties['Failed'] -and $item.Failed) {
                $results.Add([PSCustomObject]@{
                    PSTypeName     = 'CaOutcome.Drift'
                    Scenario       = $name
                    Status         = 'Failed'
                    HasChange      = $false
                    CurrentDelta   = $null
                    ProjectedDelta = $null
                    Summary        = "Evaluation failed: $($item.Error)"
                })
                continue
            }

            $entry = Get-CaProperty -InputObject $stored -Name $name
            if ($null -eq $entry) {
                $results.Add([PSCustomObject]@{
                    PSTypeName     = 'CaOutcome.Drift'
                    Scenario       = $name
                    Status         = 'Added'
                    HasChange      = $true
                    CurrentDelta   = $null
                    ProjectedDelta = $null
                    Summary        = 'Not in the baseline; new scenario or a renamed one'
                })
                continue
            }

            $currentDelta = Compare-CaEffectiveControl `
                -From (ConvertFrom-CaBaselineEntry -Entry (Get-CaProperty -InputObject $entry -Name 'current')) `
                -To $item.Current
            $projectedDelta = Compare-CaEffectiveControl `
                -From (ConvertFrom-CaBaselineEntry -Entry (Get-CaProperty -InputObject $entry -Name 'projected')) `
                -To $item.Projected

            $hasChange = $currentDelta.HasChange -or $projectedDelta.HasChange

            $parts = New-Object System.Collections.Generic.List[string]
            if ($currentDelta.HasChange) { $parts.Add("enforced now: $($currentDelta.Summary)") }
            if ($projectedDelta.HasChange) { $parts.Add("after promotion: $($projectedDelta.Summary)") }
            $summary = 'No change'
            if ($parts.Count -gt 0) { $summary = $parts -join ' | ' }

            $results.Add([PSCustomObject]@{
                PSTypeName     = 'CaOutcome.Drift'
                Scenario       = $name
                Status         = $(if ($hasChange) { 'Changed' } else { 'Unchanged' })
                HasChange      = $hasChange
                CurrentDelta   = $currentDelta
                ProjectedDelta = $projectedDelta
                Summary        = $summary
            })
        }
    }

    end {
        # Reported last so that a missing scenario reads as a gap in the run rather than as a
        # result of it. The distinction from "removed" is the point: the usual cause is a failed
        # evaluation, and a transient HTTP error must not look like a policy change.
        foreach ($name in ($storedNames | Sort-Object)) {
            if ($seen -contains $name) { continue }

            $results.Add([PSCustomObject]@{
                PSTypeName     = 'CaOutcome.Drift'
                Scenario       = $name
                Status         = 'Missing'
                HasChange      = $true
                CurrentDelta   = $null
                ProjectedDelta = $null
                Summary        = ('In the baseline but not in this run - a failed evaluation, ' +
                    'or the scenario was taken out of the matrix')
            })
        }

        $results.ToArray()
    }
}