Private/ConvertTo-CaBaselineEntry.ps1

function ConvertTo-CaBaselineEntry {
    <#
    .SYNOPSIS
        Reduces one effective outcome to the stable shape a baseline stores

    .DESCRIPTION
        A baseline is a file that gets committed, so the only thing that should ever change in
        it is the outcome. Everything here exists to make that true.

        Ordering is imposed rather than inherited. Graph returns policies in no guaranteed
        order, and a baseline that reordered its own arrays run to run would produce a diff on
        every commit, which is the fastest way to teach everybody to ignore the diff. So
        policies sort by id, choices by policy, and controls are already sorted by the fold.

        Only what Compare-CaEffectiveControl reads is stored - access, blockers, requirements,
        choices, unsatisfiable requirements, session controls and applied policies. Storing the
        whole outcome would bloat the file with the full policy conditions of every policy in
        the tenant, and none of it would be compared. Storing less would mean the comparison
        could not run against a stored baseline at all, which is the entire point.

        Session conflicts are deliberately not stored. They are a property of the live policy
        set rather than of this sign-in's outcome, they are reported by the matrix run itself,
        and including them would make a conflict appear as scenario drift in every scenario it
        touches at once.

    .PARAMETER EffectiveControl
        A Current or Projected outcome from Get-CaEffectiveControl.

    .OUTPUTS
        Ordered hashtable, ready to serialise.

    .EXAMPLE
        $entry = ConvertTo-CaBaselineEntry -EffectiveControl $outcome.Current

    .NOTES
        Author: Jeffrey Stuhr
        Version: 0.3.0
        Last Updated: 2026-08-17
    #>


    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object]$EffectiveControl
    )

    if ($null -eq $EffectiveControl) {
        return [ordered]@{}
    }

    $sessionControls = [ordered]@{}
    $controls = Get-CaProperty -InputObject $EffectiveControl -Name 'SessionControls'
    if ($null -ne $controls -and $controls.PSObject) {
        foreach ($property in ($controls.PSObject.Properties | Sort-Object Name)) {
            $sessionControls[$property.Name] = $property.Value
        }
    }

    return [ordered]@{
        access               = [string]$EffectiveControl.Access
        isEffectivelyBlocked = [bool]$EffectiveControl.IsEffectivelyBlocked
        requiredControls     = @($EffectiveControl.RequiredControls)
        blockedBy            = @(@($EffectiveControl.BlockedBy) | Sort-Object PolicyId |
            ForEach-Object {
                [ordered]@{ policyId = $_.PolicyId; policyName = $_.PolicyName }
            })
        optionalChoices      = @(@($EffectiveControl.OptionalChoices) | Sort-Object PolicyId |
            ForEach-Object {
                [ordered]@{
                    policyId = $_.PolicyId; policyName = $_.PolicyName
                    anyOf = @($_.AnyOf | Sort-Object)
                }
            })
        unsatisfiable        = @(@($EffectiveControl.UnsatisfiableRequirements) |
            Sort-Object PolicyId | ForEach-Object {
                [ordered]@{
                    policyId = $_.PolicyId; policyName = $_.PolicyName
                    operator = $_.Operator
                    controls = @($_.Controls | Sort-Object)
                    blockers = @($_.Blockers | Sort-Object)
                    reasons  = @($_.Reasons | Sort-Object)
                }
            })
        # Stored because a custom strength is editable and every policy referencing it changes
        # when it is edited, with no policy document changing at all
        strengths            = @(@($EffectiveControl.AuthenticationStrengths) |
            Sort-Object Control | ForEach-Object {
                [ordered]@{
                    control                       = $_.Control
                    id                            = $_.Id
                    displayName                   = $_.DisplayName
                    policyType                    = $_.PolicyType
                    allowedCombinations           = @($_.AllowedCombinations | Sort-Object)
                    combinationConfigurationCount = [int]$_.CombinationConfigurationCount
                }
            })
        sessionControls      = $sessionControls
        appliedPolicies      = @(@($EffectiveControl.AppliedPolicies) | Sort-Object Id |
            ForEach-Object {
                [ordered]@{ id = $_.Id; displayName = $_.DisplayName; state = $_.State }
            })
    }
}