Private/ExceptionLifecycle.ps1

function Merge-HLNoteProperty {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$InputObject,

        [Parameter(Mandatory)]
        [string]$Name,

        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $InputObject.PSObject.Properties[$Name]) {
        $InputObject | Add-Member -NotePropertyName $Name -NotePropertyValue $Value
    }
    else {
        $InputObject.$Name = $Value
    }
}

function Get-HLExceptionEffectiveStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Exception,

        [datetime]$AsOf = (Get-Date)
    )

    $status = [string]$Exception.status
    if ($status -ne 'Approved') {
        return $status
    }

    $expiry = [datetime]::MinValue
    if ((Test-HLProperty -InputObject $Exception -Name 'expires') -and
        (Test-HLDateString -Value $Exception.expires -ParsedDate ([ref]$expiry)) -and
        $expiry.Date -lt $AsOf.Date) {
        return 'Expired'
    }

    return $status
}

function Get-HLExceptionApprovalFingerprint {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Exception
    )

    $baselines = $null
    if (Test-HLProperty -InputObject $Exception -Name 'baselines') {
        $baselines = @(@($Exception.baselines | ForEach-Object { [string]$_ }) | Sort-Object -CaseSensitive)
    }
    $material = [pscustomobject][ordered]@{
        controlId            = [string]$Exception.controlId
        owner                = [string]$Exception.owner
        reason               = [string]$Exception.reason
        ticket               = [string]$Exception.ticket
        expires              = [string]$Exception.expires
        targets              = @(@($Exception.targets | ForEach-Object { [string]$_ }) | Sort-Object -CaseSensitive)
        baselines            = $baselines
        compensatingControls = @(@($Exception.compensatingControls | ForEach-Object { [string]$_ }) | Sort-Object -CaseSensitive)
        approvedBy           = if (Test-HLProperty -InputObject $Exception -Name 'approvedBy') { [string]$Exception.approvedBy } else { $null }
        approvedOn           = if (Test-HLProperty -InputObject $Exception -Name 'approvedOn') { [string]$Exception.approvedOn } else { $null }
    }
    return $material | ConvertTo-Json -Depth 10 -Compress
}

function ConvertTo-HLDraftException {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Id,

        [Parameter(Mandatory)]
        [string]$ControlId,

        [Parameter(Mandatory)]
        [string]$Owner,

        [Parameter(Mandatory)]
        [string]$Reason,

        [Parameter(Mandatory)]
        [string]$Ticket,

        [Parameter(Mandatory)]
        [datetime]$Expires,

        [Parameter(Mandatory)]
        [string[]]$Target,

        [AllowNull()]
        [string[]]$Baseline,

        [AllowNull()]
        [string[]]$CompensatingControl
    )

    $entry = [pscustomobject][ordered]@{
        id                   = $Id
        controlId            = $ControlId
        status               = 'Draft'
        owner                = $Owner.Trim()
        reason               = $Reason.Trim()
        ticket               = $Ticket.Trim()
        expires              = $Expires.ToString('yyyy-MM-dd')
        targets              = @($Target)
        compensatingControls = @($CompensatingControl | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    }
    if ($null -ne $Baseline -and @($Baseline).Count -gt 0) {
        $entry | Add-Member -NotePropertyName baselines -NotePropertyValue @($Baseline)
    }

    return $entry
}

function Assert-HLExceptionTransition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$CurrentStatus,

        [Parameter(Mandatory)]
        [string]$DesiredStatus
    )

    $allowed = @{
        Draft    = @('Draft', 'Approved', 'Revoked')
        Approved = @('Approved', 'Revoked')
        Revoked  = @('Revoked')
    }
    if (-not $allowed.ContainsKey($CurrentStatus) -or $DesiredStatus -notin $allowed[$CurrentStatus]) {
        throw "Unsupported exception transition from '$CurrentStatus' to '$DesiredStatus'."
    }
}