Public/Add-SqlSpn.ps1

# =============================================================================
# Script : Add-SqlSpn.ps1
# Author : Keith Ramsey
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-05-09 Keith Ramsey Phase 2 release polish - DR-202 standard header applied.
# 2026-05-14 Keith Ramsey Array args to the native wrapper; register against
# AccountName (setspn cannot resolve a DN); log
# SUCCESS only when setspn actually succeeded.
# 2026-05-15 Keith Ramsey Success verdict delegated to Private\
# Invoke-SqlSpnSetspnAction (shared, no drift).
# =============================================================================
function Add-SqlSpn {
    <#
    .SYNOPSIS
        Registers each SPN from a plan against the plan's AccountDn (primitive, no conflict check).
    .DESCRIPTION
        Iterates the plan's ProposedSpns and calls setspn -S for each one, then writes
        a SUCCESS entry to the audit log. This is the low-level primitive: it does NOT
        run a forest-wide duplicate check, and it does NOT honor the cross-forest -T
        flag. For the full safety-checked pipeline, use Invoke-SqlSpnExecutionEngine.

        Honors ShouldProcess, so -WhatIf and -Confirm work.
    .PARAMETER SpnPlan
        Plan object from New-SqlSpnPlan. Required fields: AccountName, ProposedSpns.
    .EXAMPLE
        $plan = New-SqlSpnPlan -VerifiedAccount $acct -Infrastructure $infra -Role Engine
        $plan | Add-SqlSpn -WhatIf
    .EXAMPLE
        New-SqlSpnPlan -VerifiedAccount $acct -Infrastructure $infra -Role Engine | Add-SqlSpn
    .OUTPUTS
        None. Side effects: setspn -S calls and audit log entries.
    .NOTES
        Use Invoke-SqlSpnExecutionEngine when you need pre-flight conflict detection
        (setspn -Q across the forest) or cross-forest support (-T flag).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory=$true, ValueFromPipeline=$true)][PSCustomObject]$SpnPlan)
    process {
        foreach ($Spn in $SpnPlan.ProposedSpns) {
            if ($PSCmdlet.ShouldProcess("Registering $Spn on $($SpnPlan.AccountName)")) {
                $r = Invoke-SqlSpnSetspnAction -ArgumentList @('-S', $Spn, $SpnPlan.AccountName)
                if ($r.Failed) {
                    Write-Error "FAILED to register $Spn on $($SpnPlan.AccountName). setspn: $($r.Output)"
                    Write-SqlSpnLog -Message "FAILED to register $Spn on $($SpnPlan.AccountName). setspn: $($r.Output)" -Level ERROR
                }
                else {
                    Write-SqlSpnLog -Message "Registered $Spn on $($SpnPlan.AccountName)" -Level SUCCESS
                }
            }
        }
    }
}