Private/New-AGDeployState.ps1

function New-AGDeployState {
    <#
    .SYNOPSIS
        Captures the pre-change deploy state for the selected CA.

    .DESCRIPTION
        Implements the state-capture half of the ADCSGoat state-file contract
        settled in issue #24. Reads the selected CA's pKIEnrollmentService
        object and records, before any AD write:

        - the selected CA identity + DN;
        - the pre-change certificateTemplates value list (exact member set);
        - the pre-change nTSecurityDescriptor, both as SDDL and as the raw
          binary form so teardown can restore it byte-for-byte.

        The Clones list starts empty; scenario deploys append one record per
        clone via Add-AGDeployStateClone. This function performs reads only.

    .PARAMETER SelectedCA
        The pscustomobject returned by Get-AGEnrollmentService (Name +
        DistinguishedName of the pKIEnrollmentService object).

    .PARAMETER Server
        The domain controller to query. Defaults to the logon server.

    .OUTPUTS
        System.Management.Automation.PSCustomObject. The in-memory state
        object, ready to persist with Save-AGDeployState.

    .EXAMPLE
        $ca = Get-AGEnrollmentService
        $state = New-AGDeployState -SelectedCA $ca
    #>

    # ShouldProcess is deliberately not implemented: this is a read-only
    # capture (LDAP reads of the Enrollment Services object). The 'New' verb
    # triggers the analyzer, but no system state is changed.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Read-only state capture; the New verb is a false positive.')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [pscustomobject]$SelectedCA,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server
    )

    begin {
        Add-Type -AssemblyName System.DirectoryServices

        if ([string]::IsNullOrEmpty($Server)) {
            $Server = [System.Net.Dns]::GetHostEntry($env:LOGONSERVER.TrimStart('\')).HostName
        }
    }

    process {
        $path = "LDAP://$Server/$($SelectedCA.DistinguishedName)"
        if (-not [System.DirectoryServices.DirectoryEntry]::Exists($path)) {
            $exception = New-Object System.InvalidOperationException("Selected CA object not found at '$path'. Cannot capture pre-change state.")
            $errorRecord = New-Object System.Management.Automation.ErrorRecord($exception, 'SelectedCAObjectNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $SelectedCA.DistinguishedName)
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $entry = New-Object System.DirectoryServices.DirectoryEntry($path)
        $entry.RefreshCache()

        $certificateTemplates = @($entry.Properties['certificateTemplates'] | ForEach-Object { "$_" })

        $sd = $entry.ObjectSecurity
        $sddl = $sd.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All)
        $binary = $sd.GetSecurityDescriptorBinaryForm()
        $entry.Dispose()

        Write-Output ([pscustomobject]@{
            SelectedCA                       = $SelectedCA
            CapturedAt                       = (Get-Date).ToUniversalTime().ToString('o')
            PreChangeCertificateTemplates    = $certificateTemplates
            PreChangeSecurityDescriptorSddl  = $sddl
            PreChangeSecurityDescriptorBinary = $binary
            Clones                           = @()
        })
    }
}