Public/Repair-NewACMEOrder.ps1

function Repair-NewACMEOrder{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Currently using Write-Host because it supports -NoNewLine')]
    param(

        [Parameter (Mandatory = $true,
            HelpMessage = "The primary / main domain associated with the ACME Order"
        )]
        $MainDomain,

        [Parameter(Mandatory=$false,
            HelpMessage="When applied, this switch will attempt to update IIS HTTPS bindings"
        )]
        [switch] $UpdateBindings,

        [Parameter(Mandatory=$false,
            HelpMessage="Comma-separated list of posts on which to update bindings. When this parameter is omitted, all HTTPS-based bindings will be updated"
        )]
        [string[]] $BindingPorts,

        [Parameter(Mandatory=$false,
            HelpMessage="When applied, this switch will skip the import of the resulting certificate into the Windows Certificate Store"
        )]
        [switch] $SkipImport,

        [Parameter(Mandatory=$false,
            HelpMessage="Specifies the Windows Certificate Store Name to import the resulting certificate into. When omitted, this parameter defaults to WebHosting"
        )]
        [ValidateScript({if($_ -in $VALIDATE_SET_CERTIFICATE_STORE_NAME) { $true } else { throw "Parameter '$_' is invalid -- must be one of: $($VALIDATE_SET_CERTIFICATE_STORE_NAME -join ",")"}})]
        [string] $StoreName = $DEFAULT_CERTIFICATE_STORE_NAME,

        [Parameter(Mandatory = $false,
            HelpMessage = "Specified the Windows Certificate Store Location to import the resulting certificate into. When omitted, this defaults to LocalMachine."
        )]
        [ValidateScript({if($_ -in $VALIDATE_SET_CERTIFICATE_STORE_LOCATION) { $true } else { throw "Parameter '$_' is invalid -- must be one of: $($VALIDATE_SET_CERTIFICATE_STORE_LOCATION -join ",")"}})]
        [string] $StoreLocation = $DEFAULT_CERTIFICATE_STORE_LOCATION,

        [Parameter(Mandatory=$false,
            HelpMessage="When applied, the script will not copy the resulting certificate files to a central location on the server"
        )]
        [switch] $SkipCentralize,

        [Parameter(Mandatory=$false,
            HelpMessage="Specifies the directory in which the resulting certificate files will be copied."
        )]
        [string] $CentralDirectory = $DEFAULT_CENTRAL_DIRECTORY,

        [Parameter(Mandatory = $false,
            HelpMessage = "Optionally write debug information about the function's execution to a file and/or the event log"
        )]
        [Switch] $debugEnabled,

        [Parameter(Mandatory = $false,
            HelpMessage = "Optionally specify a directory to write a debug log file to"
        )]
        [string] $debugLogDirectory = $DEFAULT_DEBUG_LOG_DIRECTORY,

        [Parameter(Mandatory = $false,
        HelpMessage = "Optionally specify whether to log to the windows event log (EVT), a file (file) or both (both)"
        )]
        [ValidateScript({if($_ -in $VALIDATE_SET_DEBUG_MODE) { $true } else { throw "Parameter '$_' is invalid -- must be one of: $($VALIDATE_SET_DEBUG_MODE -join ",")"}})]
        [string] $debugMode = $DEFAULT_DEBUG_MODE
    )

    # check to see if the global debug environment variable is set
    if($null -ne $env:CERTIFICAT_DEBUG_ALWAYS){
        $debugEnabled = $true
    }

    # Build a complete command of all parameters being used to run this function
    $ps5Command = "powershell.exe {import-module CertifiCat-PS -Force; $($MyInvocation.MyCommand) "
    $functionArgs = ""
    foreach($a in $PSBoundParameters.Keys){
        if($PSBoundParameters[$a] -eq $true){
            $functionArgs += "-$a "
        } else {
            $functionArgs += "-$a `"$($PSBoundParameters[$a])`" "
        }
    }
    $ps5Command += ("$functionArgs}")

    #begin building the function's return object
    $fro = [PSCustomObject]@{
        FunctionName = $myinvocation.MyCommand;
        RunningPSVersion = $PSVersionTable.PSVersion.ToString();
        PS5Command = $ps5Command;
        FunctionArguments = $functionArgs;
        FunctionSuccess = $true;
        Errors = @();
        Certificate = @();
        Bindings = @();
        CertificateImported = $true;
        BindingsUpdated = $true;
        StoreLocation = $StoreLocation;
        StoreName = $StoreName;
        PFXPath = $PfxPath;
        CertificateCentralized = $true;
        CentralDirectory = $CentralDirectory;
        CertificateFriendlyName = "";
        debugEnabled= $debugEnabled;
        debugLogDirectory = $debugLogDirectory;
        debugMode = $debugMode;
    }

    Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Attempting to repair open ACME order"

    # Check to ensure that we're running from an elevated PowerShell session
    if(!(Assert-AdminAccess)) {
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        $fro.Errors += "Session lacks administrative access. Ensure that PowerShell was run as an Administrator."
        $fro.FunctionSuccess = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
    }

    #######################################
    # Begin Parameter Pre-Fight Checks
    #######################################

    Write-Host "-> Verifying that the Posh-ACME Module is installed and available..." -NoNewline
    if(!(Assert-PSACME)){
        Write-Fail

        Write-Host "`tCould not load the Posh-ACME module... was it installed in the CurrentUser scope instead of LocalMachine? Cannot continue!" -ForegroundColor Red
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        $fro.Errors += "Posh-ACME module was not found -- it might be missing, or have been installed in the scope of a different user, rather than LocalMachine"
        $fro.FunctionSuccess = $false
        $fro.BindingsUpdated = $false
        $fro.CertificateImported = $false
        $fro.CertificateCentralized = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
    } else {
        Write-Ok
    }

    Write-Host "-> Validating incoming parameters..." -NoNewline

    # Check to see if we're going to update bindings, and, if so, if we're running in a modern (but unsupported) version of PowerShell
    if(($UpdateBindings) -and (!(Assert-PSVersion))){
        Write-Fail

        Write-Host "`tDetected this function running from a modern PowerShell console. This combination of parameters REQUIRES the use of PowerShell 6 or earlier. Check the 'PS5Command' property of the return object for a complete command to run instead." -ForegroundColor Red
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        $fro.Errors += "Function/parameters require PowerShell 6 or earlier, but running from a modern console. See the PS5Command property for a PowerShell 5 equivalent to run."
        $fro.FunctionSuccess = $false
        $fro.BindingsUpdated = $false
        $fro.CertificateImported = $false
        $fro.CertificateCentralized = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
    }

     # Check to make sure we aren't attempting to update binding(s) with a certificate that's being imported into the CurrentUser store
     if(($UpdateBindings) -and $($StoreLocation -ne "LocalMachine")){
        Write-Fail

        "`tWhen the -UpdateBindings switch is applied to this function, the -StoreLocation parameter MUST be LocalMachine!" | Write-Host -ForegroundColor Red -BackgroundColor Black
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        $fro.Errors += "UpdateBindings switch applied, but -StoreLocation parameter set to $StoreLocation. To update Site bindings, StoreLocation MUST be LocalMachine"
        $fro.FunctionSuccess = $false
        $fro.BindingsUpdated = $false
        $fro.CertificateImported = $false
        $fro.CertificateCentralized = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
     }

    # Check to ensure that we aren't attempting to update IIS bindings but skipping the import into the cert store
    if(($UpdateBindings) -and ($SkipImport)){
        Write-Fail

        "`tWhen the -UpdateBindings switch is applied to this function, you cannot also specify the -SkipImport switch!" | Write-Host -ForegroundColor Red -BackgroundColor Black
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        $fro.Errors += "UpdateBindings and SkipImport switches both specified -- When UpdateBindings is specified, SkipImport must NOT be present"
        $fro.FunctionSuccess = $false
        $fro.BindingsUpdated = $false
        $fro.CertificateImported = $false
        $fro.CertificateCentralized = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
    }

    Write-Ok

    # Check to see if the SkipCentralize switch is applied alongside a custom CentralDirectory parameter
    # (We'll just warn the user and ignore the CentralDirectory param -- we won't do a hard-stop)
    if(($SkipCentralize) -and ($CentralDirectory -ne $DEFAULT_CENTRAL_DIRECTORY)){
        Write-Host "`tWARNING: Ignoring the -CentralDirectory parameter value due to the presence of the -SkipCentralize parameter!" -ForegroundColor Yellow -BackgroundColor Black
    }

    # Check to see if the -BindingPorts parameter is populated, without the -UpdateBindings switch
    # (We'll just warn the user and ignore the binding updates -- we won't do a hard-stop)
    if(($BindingPorts -ne "") -and ($null -ne $BindingPorts) -and (!$UpdateBindings)){
        "`tWARNING: The -BindingPorts parameter was populated, but the -UpdateBindings switch was not specified! Bindings WILL NOT be updated automatically!" | Write-Host -ForegroundColor Yellow
    }

    # Update the certificate central directory with the primary domain name and timestamp
    $centralDirectory = "$centralDirectory\$($DomainList[0])\$(get-date -format "MM-dd-yyyy-HH-mm-ss")"

    #######################################
    # End Parameter Pre-Fight Checks
    #######################################

    # check to see if the status of the current order is valid
    Write-Host "-> Querying ACME Server for Current order status..." -NoNewline

    $activeOrder = Get-PAOrder -MainDomain $MainDomain -Refresh

    # add the certificate / order's friendlyname to the return object
    $fro.CertificateFriendlyName = $activeOrder.FriendlyName

    switch($activeOrder.Status){
        "processing"{
            Write-Fail
            Write-Host "`tActive order is still being processed by the ACME server - please try again later" -ForegroundColor Red

            $fro.FunctionSuccess = $false
            $fro.BindingsUpdated = $false
            $fro.CertificateImported = $false
            $fro.CertificateCentralized = $false
            $fro.Errors += "Active order is still being processed by the ACME server - please try again later"

            Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

            # write debug information if desired
            if($debugEnabled){
                Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
            }

            return $fro
        }
        "valid"{
            Write-Ok
        }
        default{
            Write-Fail
            Write-Host "`tActive order is in a status we aren't expecting (status = $($activeOrder.Status))" -ForegroundColor Red

            $fro.FunctionSuccess = $false
            $fro.BindingsUpdated = $false
            $fro.CertificateImported = $false
            $fro.CertificateCentralized = $false
            $fro.Errors += "Active order is in a status we aren't expecting (status = $($activeOrder.Status))"

            Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

            # write debug information if desired
            if($debugEnabled){
               Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
            }

            return $fro
        }
    }

    Write-Host "-> Completing ACME Order..." -NoNewLine
    $cert = Complete-PAOrder

    if($null -ne $cert.Thumbprint){
        Write-Ok
    } else {
        Write-Fail
        Write-Host "`t`tFailed to complete ACME order -- no certificate was returned" -ForegroundColor Red

        $fro.FunctionSuccess = $false
        $fro.BindingsUpdated = $false
        $fro.CertificateImported = $false
        $fro.CertificateCentralized = $false
        $fro.Errors += "Failed to complete ACME order -- no certificate was returned"

        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
        }

        return $fro
    }

    # copy the certificate to a central location, if needed
    $CertCurrentDirectory = $cert.certfile.replace("cert.cer", "")

    if($SkipCentralize){
        Write-Skipped
        $fro.PFXPath = "$CertCurrentDirectory\cert.pfx"
    } else {
        $centralizedOK = Copy-CertificateToCentralDirectory $CentralDirectory $CertCurrentDirectory
        switch($centralizedOK){
            "directory"{
                Write-Host "`t`tAn error occurred creating new directory -- copy cannot continue!" -ForegroundColor Red

                $fro.FunctionSuccess = $false
                $fro.BindingsUpdated = $false
                $fro.CertificateImported = $false
                $fro.CertificateCentralized = $false
                $fro.Errors += "Error occurred attempting to create central directory '$CentralDirectory'"

                Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

                # write debug information if desired
                if($debugEnabled){
                    Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
                }

                return $fro
            }

            "copy"{
                Write-Host "`t`tDue to a failure copying the certificate files, we won't proceed with any additional actions!" -ForegroundColor Red

                $fro.FunctionSuccess = $false
                $fro.BindingsUpdated = $false
                $fro.CertificateImported = $false
                $fro.CertificateCentralized = $false
                $fro.Errors += "Error occurred attempting to copy certificate files from '$CertCurrentDirectory' to '$CentralDirectory'"

                Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

                # write debug information if desired
                if($debugEnabled){
                    Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
                }

                return $fro
            }

            default{
                $fro.PFXPath = "$CentralDirectory\cert.pfx"
            }
        }
    }

    # import the resultant certificate into the windows certificate store
    Write-Host "-> Importing certificate into the $StoreLocation\$StoreName Store..." -NoNewline
    if($SkipImport){
        Write-Skipped
    } else {
        # use the Posh-ACME function to do the heavy lifting here
        Install-PACertificate $cert -StoreLocation $StoreLocation -StoreName $StoreName

        #make sure we imported the certificate successfully
        $importedCert = get-item "cert:\$StoreLocation\$StoreName\$($cert.thumbprint)"

        if($null -ne $importedCert){
            Write-Ok
            $fro.Certificate = $importedCert
        } else {
            Write-Fail
            Write-Host "`tDue to a failure importing the new certificate into the certificate store, we won't proceed with any additional actions!" -ForegroundColor Red

            Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

            $fro.FunctionSuccess = $false
            $fro.BindingsUpdated = $false
            $fro.CertificateImported = $false
            $fro.CertificateCentralized = $false
            $fro.Errors += "Error occurred attempting to import new certificate into cert:\$CertLocation\$CertStore"

            # write debug information if desired
            if($debugEnabled){
                Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
            }

            return $fro
        }
    }

    # check to see if we're going to update IIS bindings
    Write-Host "-> Updating IIS Site Bindings..." -NoNewLine
    if($UpdateBindings){
        Write-Pending
        $updatedBindings = Update-IISBindings $BindingPorts $StoreName $importedCert.Thumbprint

        #remove the phantom $nulls appearing in the list
        $updatedBindings = $updatedBindings | where-object {$_ -ne $Null }
        $fro.Bindings = $updatedBindings

        if(($updatedBindings | Where-Object {$_.UpdatedSuccessfully -eq $false}).Count -eq 0){
            Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed successfully!" "green"

            # write debug information if desired
            if($debugEnabled){
                Write-ACMEDebug $myInvocation.MyCommand $fro $true $debugMode $debugLogDirectory
            }

            return $fro
        } else {
            Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed unsuccessfully!" "red"

            $fro.Errors += "Error(s) occurred updating one or more bindings. Please review the Bindings property of this return object for more details."
            $fro.BindingsUpdated = $false

            # write debug information if desired
            if($debugEnabled){
                Write-ACMEDebug $myInvocation.MyCommand $fro $false $debugMode $debugLogDirectory
            }

            return $fro
        }
    } else {
        Write-Skipped
        Write-FunctionBlock "[$($myinvocation.MyCommand)]" "Completed successfully!" "green"
        $fro.BindingsUpdated = $false

        # write debug information if desired
        if($debugEnabled){
            Write-ACMEDebug $myInvocation.MyCommand $fro $true $debugMode $debugLogDirectory
        }

        return $fro
    }

}