Public/Set-RiskAcceptance.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Set-RiskAcceptance {
    <#
    .SYNOPSIS
        Accepts risk on a specific audit check ID with justification.
    .DESCRIPTION
        Records a risk acceptance decision for an audit check. The accepted check will
        be flagged as ACCEPTED in reports instead of FAIL. Acceptances can have expiration
        dates, after which they auto-expire and the check reverts to its actual status.
    .PARAMETER CheckId
        The audit check ID to accept risk on (e.g., AUTH-003, ADPWD-005).
    .PARAMETER Justification
        Required written justification for accepting the risk.
    .PARAMETER AcceptedBy
        Name or email of the person accepting the risk.
    .PARAMETER ExpirationDays
        Number of days until this acceptance expires. Default: 365. Set to 0 for no expiration.
    .PARAMETER ConfigPath
        Override the risk acceptance file path.
    .EXAMPLE
        Set-RiskAcceptance -CheckId AUTH-003 -Justification 'Security keys not feasible for remote staff' -AcceptedBy 'jsmith@district.edu'
    .EXAMPLE
        Set-RiskAcceptance -CheckId ADPWD-005 -Justification 'Legacy system compatibility' -ExpirationDays 90 -AcceptedBy 'IT Director'
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory)]
        [string]$CheckId,

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

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

        [ValidateRange(0, 3650)]
        [int]$ExpirationDays = 365,

        [Alias('RuntimeConfig')]
        [string]$ConfigPath
    )

    if (-not $PSCmdlet.ShouldProcess($CheckId, "Accept risk (by $AcceptedBy, expires in $ExpirationDays days)")) {
        return
    }

    $riskPath = if ($ConfigPath) {
        $ConfigPath
    } else {
        Join-Path (Get-PSGuerrillaDataRoot) 'risk-acceptance.json'
    }

    $riskDir = Split-Path $riskPath -Parent
    if (-not (Test-Path $riskDir)) {
        New-Item -Path $riskDir -ItemType Directory -Force | Out-Null
    }

    # Load existing acceptances
    $acceptances = @{}
    if (Test-Path $riskPath) {
        try {
            $acceptances = Get-Content -Path $riskPath -Raw | ConvertFrom-Json -AsHashtable
        } catch {
            $acceptances = @{}
        }
    }
    if (-not $acceptances.entries) { $acceptances.entries = @{} }

    $now = [datetime]::UtcNow
    $expiration = if ($ExpirationDays -gt 0) {
        $now.AddDays($ExpirationDays).ToString('o')
    } else {
        $null
    }

    $acceptances.entries[$CheckId] = @{
        checkId       = $CheckId
        justification = $Justification
        acceptedBy    = $AcceptedBy
        acceptedOn    = $now.ToString('o')
        expiresOn     = $expiration
    }
    $acceptances.lastModified = $now.ToString('o')

    $acceptances | ConvertTo-Json -Depth 5 | Set-Content -Path $riskPath -Encoding UTF8

    return [PSCustomObject]@{
        PSTypeName    = 'PSGuerrilla.RiskAcceptance'
        CheckId       = $CheckId
        Justification = $Justification
        AcceptedBy    = $AcceptedBy
        AcceptedOn    = $now
        ExpiresOn     = if ($expiration) { [datetime]$expiration } else { $null }
        Status        = 'ACCEPTED'
        Path          = $riskPath
    }
}