PowerShell script for configuring secure connections

Some of the SAFEQ 6 communication links are not encrypted by default. Usually this is not a concern for environment with a single server, because the communication takes place on a localhost. However, in scenarios involving multiple servers or when deploying the client application on a workstation, it's strongly advised to enhance the security of communication links following the documentation. Manual execution of this process can be time-consuming, taking a few hours, and must be repeated upon certificate expiration.

Use the PowerShell automation script below to perform the most important changes in a few minutes.

The script requires CSR signing, for hints on the process see Working with certificates in Keystore Explorer.

SAFEQ6_hardening.ps1
<#
.SYNOPSIS
  Securing communication between SAFEQ 6 SPOC and other components.

.ABBREVIATIONS
  - Extension .pfx and .p12 typically refer to the same file format and represents PKCS#12 - Personal Information Exchange format.
  - SAN means Subject Alternative Name, list of names and addresses to be associated with the certificate.
  - CA is certificate authority (CA) used to sign certificates, making new certificates trustworthy to all clients who already trust CA.

.DESCRIPTION
  The script automates steps described in the following documentation:
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-secured-communication-with-spooler-control
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-secured-communication-between-management-s
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-the-secure-connection-between-cluster-node
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-server-spooler-authentication-for-job-tran
  https://docs.ysoft.cloud/safeq6/latest/safeq6/configuring-secured-connection-between-terminals-a
  https://docs.ysoft.cloud/safeq6/latest/safeq6/mobile-integration-gateway-deployment               (certificate replacement part)
  https://docs.ysoft.cloud/safeq6/latest/safeq6/configuring-ssl-tls-for-ysoft-safeq-management-ser
  https://docs.ysoft.cloud/safeq6/latest/safeq6/configuring-ssl-tls-for-end-user-interface
  https://docs.ysoft.cloud/safeq6/latest/safeq6/configuring-ssl-tls-for-ysoft-safeq-payment-system
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-up-https                                    (relates to HTTPS of JS and V3)

  Order of steps:
  - admin provides JKS with new CA signed certificate / or script generates new certificate and CSR, then admin signs it by CA
  - script reads keystore/truststore locations and passwords of each SAFEQ component from SAFEQ config files
  - script adds new certificate to the keystore/truststore/WindowsCertificateStore and alters SAFEQ config files to use new certificate
    location and password of existing keystore/truststore is retained (except for Management communicator, this one uses new files)
    old certificates in keystore/truststore/WindowsCertificateStore are left intact (script is not removing expired certificates etc.)
    each file to be modified is backed up by the script before modification takes place
  
  The script creates log file that can be used to review actions taken (path in param $logfile).


.PREREQUISITE
  The script needs to be launched as administrator.
  The same CA is used to sign all certificates in SAFEQ multi-server environment.

  If using new certificate provided by customer
    certificate is provided in JKS format
    keystore file contains entire certificate chain (CA cert and Signed certificate)
    a single password secures both the JKS and its contained certificates
    certificate SAN includes all FQDNs to be associated with new certificate and load balancer FQDN (if used); IP addresses are must when used in config files or for web access
    example to generate JKS and key pair, then CSR:
        keytool -genkeypair -keyalg RSA -keysize 2048 -alias hardeningcert -keystore custom.jks -validity 365
        keytool -certreq -keystore custom.jks -alias hardeningcert -keyalg RSA -storepass yourpassword -file casigningrequest.csr -ext SAN=dn:server01.ysoft.cz,ip:10.0.10.10 -ext "keyUsage=digitalSignature,keyEncipherment,nonRepudiation" -ext "extendedKeyUsage=serverAuth,clientAuth" -ext "basicConstraints:Critical=CA:false"
        .csr needs to be signed by CA, new file created by signing needs to be imported back to JKS
        details at https://docs.ysoft.cloud/safeq6/latest/safeq6/system-communication-hardening

  If allowing script to generate new certificate
    admin is available to sign CSR by CA upon script request


.LIMITATIONS
  The script requires presence of YSoftSQ-SPOC or YSoftSQ-Management service (i.e. FlexiSpooler server in DMZ missing those services needs
  to be reconfigured manually per the relevant documentation).

  The script needs to be launched on every SPOC and Management server in SAFEQ multi-server environment.

  Any customization or product extension communicating to SPOC or Management needs to be reconfigured manually for a secure communication per 
  its own documentation.

  Any workstation client FlexiSpooler needs to be set for secure communication once the script completes, otherwise it will not function: 
  https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-secured-communication-with-spooler-control
  Custom MSI package for securing client PC FlexiSpooler can be ordered:
  https://portal.ysoft.com/customer-support-service/professional-services/msi-dmg-packages

  Script can replace Web Server certificate only when Tomcat keystore is in JKS format (this is default), otherwise script reports warning and skips
  this particular action. Some server.xml syntax at environment updated from very old build may also raise a warning and be skipped.
    
  Script replaces only the certificates listed in the description, other certificates must be replaced per the documentation if required, such as:
  Job Service IdentityServerOptions.SigningCertificateOptions.Thumbprint in local.json
  (procedure requires the same signing certificate for all YSoft SafeQ Job Services in the cluster, for other components  it would be a good practice to use unique cert per server)
  
  Script is not securing Distributed Layer Communication between SPOC Group members, perform that manually if needed:
  https://docs.ysoft.cloud/safeq6/latest/safeq6/how-to-secure-distributed-layer-communication
  (procedure requires the same SPOC Group communicator cert on all servers, for other components it would be a good practice to use unique cert per server)

  Script is not securing ETCD communication for cluster deployment, perform that manually if needed:
  https://docs.ysoft.cloud/safeq6/latest/safeq6/hardening-etcd-communication-security

  Script not tested on Multi-Tenant deployment.

  To revert to previous configuration
  1. use the following command (replace installation dir and backup extension with yours):
     Get-ChildItem 'C:\SAFEQ6' -Recurse | ? {$_.Name -like '*.bck20263422_20h34m22s' } | foreach { Copy-Item -Path $_.FullName -Destination $($_.FullName -replace '.bck20263422_20h34m22s') -Force }
  2. delete <SPOC>\conf\communicator.conf
  3. delete <MGMT>\conf\communicator.conf
  4. restart SAFEQ services


.NOTES
  Version:        0.16
  Last Modified:  30/Jan/2026
  Tested with SAFEQ 6 Build 114
        
.EXAMPLE
  Define values in [Parameters] section.
  Run Windows PowerShell as an administrator and launch the command as follows:
  c:\users\administrator\downloads> .\scriptname.ps1
  Follow on-screen instructions (some actions require manual step)


#>

#-----------------------------------------------------------[Parameters]-----------------------------------------------------------
# Set these parameters manually before launching the script

# Directory for script-generated temporary files.
#  The script creates the folder if it doesn't exist, clears its content at the start,
#  and can be manually deleted after completion.
$workDir = "$($env:homedrive)\temp\SAFEQhardening"

# Clear content of $workDir at startup automatically?
#  $true: delete without user confirmation
#  $false: prompt user for deletion confirmation
$workDirAutoclean = $false

# File path for logging script activities
# Example:
$logfile = $($workDir) + '\SAFEQhardening.log'

# Generate new certificate?
#  $true: script generates new certificate and asks for CSR signing by CA
#  $false: script loads new CA signed certificate from JKS provided by customer
$generatenewcert = $true

# Apply the new certificate for Terminal Server also? ($true or $false)
# Terminal Server certificate is presented to printer clients (Embedded Terminals).
# CA used to sign the new certificate must be trusted by the printer clients, otherwise certificate trust error may occur.
$replaceTScert = $false

# Apply the new certificate for Mobile Integration Gateway also? ($true or $false)
# Mobile Integration Gateway is presented to IPPS clients (e.g. workstation that is mapping printer via IPPS, cellphones...).
# CA used to sign the new certificate must be trusted by the IPPS clients, otherwise certificate trust error may occur.
$replaceMIGcert = $false

# Apply the new certificate for HttpServerOptions of Job Service and V3 server spooler also? ($true or $false)
# Job Service and V3 server spooler HTTPS certificate is presented to V3 spooler on workstations.
# CA used to sign the new certificate must be trusted by client workstations, otherwise certificate trust error may occur.
$replaceJScert = $false


# SECTION RELATED TO CERTIFICATE CREATED BY CUSTOMER
# 
# applies only with $generatenewcert=$false

# Path to JKS file created by customer
#  file must be outside $workDir
$preconfiguredJKS = 'C:\temp\example.jks'

# Alias of certificate in JKS file created by customer
#  command listing all aliases > keytool.exe -v -list -keystore <pathtokeystore>
$preconfiguredJKSalias = 'somealias'

# Password protecting customer's JKS file and certificate inside
$preconfiguredJKSpass = 'somepassword'



# SECTION RELATED TO CERTIFICATE AUTO-GENERATED BY SCRIPT
#
# applies only with $generatenewcert=$true

# Validity in days 
# once expired, certificates need to be replaced; admin is able to override this during CSR signing
$keyvalidity = 365

# Distinguished Name (DN)
# example: 'CN=SAFEQhardening, OU=it, O=ysoft, L=Brno, S=Czechia, C=CZ'
$certDN = 'CN=SAFEQhardening, OU=it, O=ysoft, L=Brno, S=Czechia, C=CZ'

# Subject Alternative Name (SAN)
# define IPs and FQDNs to be associated with new certificate, include load balancer IP and FQDN (if used)
# or set autodetect (do not use this with load balancer; automatic detection of all IPv4 addresses and FQDN for local server)
# example: '10.0.10.10','10.0.10.20','server01.ysoft.cz','server02.ysoft.cz'
# example: 'autodetect'
$certAddr = 'autodetect'


#-----------------------------------------------------------[Execution]------------------------------------------------------------

function Generate-RandomString {
    param(
        [int]$length = 15
    )

    $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    $random = New-Object -TypeName System.Random
    $randomString = -join ((1..$length) | ForEach-Object { $characters[$random.Next(0, $characters.Length)] })

    return $randomString
}

function Create-Backup ($in) {
    $out = $($in + '.bck' + $starttime)

    if (!(Test-Path $in)){
        Write-HostGray " $in does not exist, script will create it"
    }
    else {
        # performing backup
        try {
            Copy-Item -Path $in -Destination $out -Force -ErrorAction Stop
            if ($?) {
                Write-HostGray " backup at $out"
            }
        }
        catch {
            Write-HostRed "ERROR copying $in to $out"
            Write-HostRed " $_"
            Write-HostRed " resolve the error, then run the script again."
            Read-Host ' press any key to terminate'
            Throw
        }
    }
}

function Write-HostWhit ($message) {
    Write-Host $message -ForegroundColor White
    $message | Out-File -FilePath $logfile -Append -Encoding utf8
}

function Write-HostGray ($message) {
    Write-Host $message -ForegroundColor Gray
    $message | Out-File -FilePath $logfile -Append -Encoding utf8
}

function Write-HostYell ($message) {
    Write-Host $message -ForegroundColor DarkYellow
    $message | Out-File -FilePath $logfile -Append -Encoding utf8
}

function Write-HostRed ($message) {
    Write-Host $message -ForegroundColor Red
    $message | Out-File -FilePath $logfile -Append -Encoding utf8
}

function Write-HostPink ($message) {
    Write-Host $message -BackgroundColor Magenta -ForegroundColor White
    $message | Out-File -FilePath $logfile -Append -Encoding utf8
}

function Get-Time {
    $(Get-Date).ToString("HH:mm:ss")
}

function Read-TomcatXMLdetails {
    param (
        [string]$XmlPath,
        [string]$SectionPath,
        [array] $LookupParameters
    )

    # Load XML content
    [xml]$xmlContent = Get-Content -Path $XmlPath -Raw

    # Iterate through all Connector elements defined by SectionPath
    foreach ($connector in (Invoke-Expression ('$xmlContent.' + $SectionPath)) ) {

        # Initialize helper variables for new-style Tomcat SSL configuration
        # These may or may not exist depending on Tomcat version / component
        $sslHostConfig = $null
        $certificate   = $null

        # Try to locate SSLHostConfig (used by newer Tomcat versions)
        try { $sslHostConfig = $connector.SelectSingleNode("SSLHostConfig") } catch { <# ignore – SSLHostConfig not present in legacy XML #> }
        # Try to locate Certificate element (used by newer Tomcat versions)
        try { $certificate = $connector.SelectSingleNode("SSLHostConfig/Certificate") } catch { <# ignore – Certificate not present in legacy XML #> }

        # Hashtable that will store resolved connector parameters
        $connectorDetails = @{}
        # Flag indicating whether this connector satisfies all required parameters
        $connectorValid = $true

        # Iterate through all parameters required by the caller
        foreach ($param in $LookupParameters) {

            # 1) Try to read parameter directly from Connector (legacy Tomcat configuration)
            $parameterValue = $connector.$param

            # 2) If not found, try SSLHostConfig (new-style Tomcat configuration – e.g. truststore*)
            if ([string]::IsNullOrEmpty($parameterValue) -and $sslHostConfig) {
                $parameterValue = $sslHostConfig.$param
            }

            # 3) If still not found, try Certificate (new-style Tomcat configuration – certificateKeystore*)
            if ([string]::IsNullOrEmpty($parameterValue) -and $certificate) {
                $parameterValue = $certificate.$param
            }

            # If parameter cannot be resolved anywhere, this connector is not suitable
            if ([string]::IsNullOrEmpty($parameterValue)) {
                $connectorValid = $false
                break
            }

            # Store resolved parameter value
            $connectorDetails[$param] = $parameterValue
        }

        # If all required parameters were resolved, return the result to caller
        if ($connectorValid -and $connectorDetails.Count -eq $LookupParameters.Count) {
            return $connectorDetails
        }
    }

    # No connector matched all required parameters
    Write-HostRed " ERROR Found 0 connectors in $($XmlPath -replace '^.*\\','') having all these parameters set:"
    Write-HostRed " $($LookupParameters -join ', ')"
    return $null
}


function Update-XmlSection {
    param (
        [string]$XmlPath,
        [string]$SectionPath,
        [hashtable]$KeyValuePairs,
        [string]$TomcatConnectorPort,    # used for TomcatXML update in specific connector based on port number
        [switch]$SkipBackup
    )

    # Load XML content
    [xml]$xmlContent = Get-Content -Path $XmlPath


    # Use specific set of steps for MPS XML (this conf has different XML syntax than other components)
    if ($SectionPath -in ('mps.communicator', 'mps.http')) {

        $SectionPath = $SectionPath -replace '^.*\.'

        $section = $xmlContent.mps.$SectionPath

        if (-not $section) {
            # Section is missing, create it
            $section = $xmlContent.CreateElement($SectionPath)
            [void]$xmlContent.mps.AppendChild($section)
        }

        foreach ($key in $KeyValuePairs.Keys) {
            # Add or update the attribute
            $section.SetAttribute($key, $KeyValuePairs[$key])
        }
    }


    # Use specific set of steps for Tomcat XML
    elseif ($SectionPath -match 'Server\.Service\.Connector') {

        # Select the Connector element based on Tomcat port number
        $connector = $xmlContent.SelectSingleNode("//Connector[@port='$TomcatConnectorPort']")

        if (-not $connector) {
            Write-HostRed " ERROR connector with port $TomcatConnectorPort not found, set up new web server certificate manually."
            $problemcnt = $problemcnt + 1
            return
        }

        # Detect new-style Tomcat SSL configuration nodes (may or may not exist)
        # SSLHostConfig       -> holds truststore* attributes in some configs (e.g. EUI)
        # SSLHostConfig/Certificate -> holds certificate* attributes (MNGMT / YPS / EUI)
        $sslHostConfig = $connector.SelectSingleNode("SSLHostConfig")
        $sslHostConfigCer = $connector.SelectSingleNode("SSLHostConfig/Certificate")

        # Apply provided key/value updates
        foreach ($key in $KeyValuePairs.Keys) {
            $value = $KeyValuePairs[$key]

            # New-style Tomcat: truststore* belongs to SSLHostConfig
            if ($key -match '^truststore' -and $sslHostConfig) {
                $sslHostConfig.SetAttribute($key, $value)
                continue
            }

            # New-style Tomcat: certificate* belongs to Certificate
            if ($key -match '^certificate' -and $sslHostConfigCer) {
                $sslHostConfigCer.SetAttribute($key, $value)
                continue
            }

            # Backward-compatible default:
            # - legacy Tomcat stores everything directly on Connector
            $connector.SetAttribute($key, $value)
        }
    }

    # Use specific set of steps for generic 'configuration.appSettings' XML type
    else {

        $SectionPath = $SectionPath -replace '^.*\.'

        # Select the specified section
        $section = $xmlContent.configuration.SelectSingleNode($SectionPath)
    
        # Check if the section exists; if not, create it
        if ($section -eq $null) {
            $section = $xmlContent.CreateElement($SectionPath)
            [void]$xmlContent.DocumentElement.AppendChild($section)
        }

        foreach ($key in $KeyValuePairs.Keys) {
            $value = $KeyValuePairs[$key]

            # Check if the key exists
            $existingKey = $section.SelectSingleNode("add[@key='$key']")

            # Create a new key element
            $newKey = $xmlContent.CreateElement("add")
            $newKey.SetAttribute("key", $key)
            $newKey.SetAttribute("value", $value)

            # Update or add the key
            if ($existingKey -eq $null) {
                [void]$section.AppendChild($newKey)
            } else {
                # Compare existing and new value
                $existingValue = $existingKey.GetAttribute("value")
                if ($existingValue -ne $value) {
                    $existingKey.SetAttribute("value", $value)
                }
            }
        }
    }

    # Backup the original file only if modifications are made
    if ($xmlContent -ne [xml](Get-Content -Path $XmlPath)) {
        if (-not $SkipBackup) {
            Create-Backup $XmlPath
        }
        
        $xmlContent.Save($XmlPath)
        Write-HostGray ' altering finished'
    } else {
        Write-HostGray ' no modifications were necessary'
    }
}

function Update-RAWconf {
    param (
        [string]$ConfPath,
        [string]$NewValues
    )

    Create-Backup $ConfPath

    if (Test-Path $ConfPath) {
        $OriginalValues = Get-Content -Path $ConfPath -Raw
    } 
    else {
         $OriginalValues = ''
    }

    # Get content of plain text configuration file
    $newConfig = ConvertFrom-StringData $NewValues

    # Iterate through lines in the original configuration, replace old values with the new ones
    $updatedConfig = @()
    foreach ($line in $OriginalValues -split "`r`n") {
        if ($newConfig.count -lt 1 -or $line -eq '') {
            # all new values were already merged, nothing left to compare this line with > add it to the new config
            $updatedConfig += $line
            continue
        }       
        if ($line -match '^#') {
            # line is commented, add it to new config file too and avoid other checks for this line (return to the start of the loop)
            $updatedConfig += $line
            continue
        }
        if ($line -match '^\s*([^=]+)=.*$') {
            $key = $matches[1].Trim()
            if ($newConfig.ContainsKey($key)) {
                # Replace the value of existing config with the new value, remove new config from further checks
                $newValue = $newConfig[$key]
                $updatedConfig += "$key=$newValue"
                $newConfig.Remove($key)
            } else {
                # If the key is not in the new configuration, use the original line
                $updatedConfig += $line
            }
        }
    }

    # Any new parameter that was not added yet in the loop above will be added to the new config here:
    if ($newConfig.count -ge 1) {
        foreach ($new in $newConfig.GetEnumerator() | Sort-Object Name) {
           $updatedConfig += "$($new.Name)=$($new.Value)"
        }
    }
    $updatedConfig = $updatedConfig -join "`r`n"
    $updatedConfig | Set-Content -Path $ConfPath
    Write-HostGray ' altering finished'
}

function Assert-WindowsTrustForThumbprint {
    param(
        [string]$checkthumbprinttrust
    )

    $cert = Get-ChildItem -Path Cert:\LocalMachine\My | ? {$_.Thumbprint -eq $checkthumbprinttrust} | Select-Object -First 1

    if (-not $cert) {
        Write-HostRed $((Get-Time) +  " ERROR certificate $checkThumbprint not found in Cert:\LocalMachine\My")
        Read-Host ' press any key to terminate'
        Throw
    }

    $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain

    # Check cert trust the similar way Windows UI would do when double-clicking .cer file, but avoid hanging on CRL/OCSP in locked-down servers
    $chain.ChainPolicy.RevocationMode  = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
    $chain.ChainPolicy.RevocationFlag  = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EntireChain
    $chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::NoFlag

    $ok = $chain.Build($cert)

    if (-not $ok) {
        $status = ($chain.ChainStatus | ForEach-Object { $_.Status.ToString() }) -join ', '
        $info   = ($chain.ChainStatus | ForEach-Object { $_.StatusInformation.Trim() }) -join ' | '

        Write-HostRed $((Get-Time) + " ERROR Windows does NOT trust the imported certificate (thumbprint $($cert.Thumbprint)).")
        Write-HostRed " chainStatus: $status"
        Write-HostRed " details    : $info"
        Write-HostRed " file       : $secCERfile"
        Write-HostRed " See additional details by double-clicking the file."
        Write-HostRed " Use the information above to refine the certificate generation process, then rerun the script."
        Read-Host ' press any key to terminate'
        Throw

    }

    Write-HostGray " Windows trust check OK for thumbprint $($cert.Thumbprint)"
    return $cert
}

$script:WinImportdone = $false
function Import-Cert-Windows {

    # list of services on each server can differ > each service calls this function separately
    # $script:WinImportdone guarantees import is performed only once for the whole script execution
    if ($WinImportdone -eq $false){
        
        # import p12 to Windows Certificate store
        Write-HostWhit $((Get-Time) + ' Importing p12 to Cert:\LocalMachine\My')
        $commandOutput = & certutil.exe -p $secpass -csp "Microsoft Enhanced RSA and AES Cryptographic Provider" -importpfx "My" "$secp12file"
        if ($LASTEXITCODE -eq 0) {
           $secwincertstore = Get-ChildItem -Path Cert:\LocalMachine\My | ? {$_.Thumbprint -eq $secthumbprint}
            Write-HostGray ' import successful'
            Write-HostGray " thumbprint $secthumbprint"
            "`r`ndetails about p12 import to Windows Certificate store:" | Out-File $secmeta -Append -Force
            ($secwincertstore | Format-List PSParentPath, Subject, FriendlyName, Issuer, Thumbprint, NotBefore, NotAfter, HasPrivateKey, EnhancedKeyUsageList | Out-String).Trim() | Out-File $secmeta -Append -Force
            $script:secwincertstoreSAN = ($secwincertstore.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.17' } | ForEach-Object {$_.Format(0)})
            $script:secwincertstoreSAN -replace 'IP Address=|DNS Name=' -replace '^','SANlist              : ' | Out-File $secmeta -Append -Force
        }

        # make CA trusted on server
        Write-HostWhit $((Get-Time) + ' Importing CA cert to Cert:\LocalMachine\Root')
        $winCAstore = Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root -FilePath $CAfile
        if ($winCAstore -match 'Thumbprint'){
            Write-HostGray ' import successful'
            Write-HostGray " CA thumbprint $CAthumbprint"
            "`r`ndetails about CA import to Windows Certificate store:" | Out-File $secmeta -Append -Force
            ($winCAstore | Format-List PSParentPath, Subject, FriendlyName, Issuer, Thumbprint, NotBefore, NotAfter, HasPrivateKey| Out-String).Trim() | Out-File $secmeta -Append -Force
        }

        # Verify OS trusts the imported leaf cert - without trust web would show cert errors and WPS would fail to start
        Assert-WindowsTrustForThumbprint -checkthumbprinttrust $secthumbprint | Out-Null

        # prevent subsequent launches of import
        $script:WinImportdone = $true
    }
}



function JSONsort ($config) {

    # Function sorts JSON LVL1 alphabetically, this helps with later binary comparison of before/after change
    $configsorted = New-Object PSObject
    foreach($Property in $config | Get-Member -type NoteProperty, Property){
        $configsorted | Add-Member -MemberType NoteProperty -Value $config.$($Property.Name) -Name $Property.Name 
    }

    return $configsorted
}

function JSONAddPropertyRecurse($orig, $addition){
    # Function related adding new configuration to JSON
    #   Properties missing in orig JSON are added
    #   Properties existing in both JSONs are set to value of second JSON
    # Code guarantees array on output, JS may require array in some properties and not in the others which might cause trouble otherwise


    if($addition -is [system.array]) {
        return ,$addition;
    }
    if($orig.GetType().Name -eq "PSCustomObject"){
        foreach($Property in $orig | Get-Member -type NoteProperty, Property){
            if($addition.$($Property.Name) -eq $null){
              $addition | Add-Member -MemberType NoteProperty -Value $orig.$($Property.Name) -Name $Property.Name
            }
            else{
               $addition.$($Property.Name) = JSONAddPropertyRecurse $orig.$($Property.Name) $addition.$($Property.Name)
            }
        }
    }
    return $addition
}

# Setting preferences to value required for keytool error handling (this value is typically a default value)
$ErrorActionPreference = 'Continue'

# Set English language in script's scope, this won't affect machine and user variables; required for proper parsing of keytool results
[Environment]::SetEnvironmentVariable('JAVA_TOOL_OPTIONS', '-Duser.language=en', 'Process')

# Admin rights check
If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544'))) {
    Write-Warning 'Administrative rights are missing. Please re-run the script as an Administrator.'
    Write-host 'Press any key to exit the script.'
    Read-Host
    exit
}

# Create working directory
If (!(Test-Path $workDir)) {
    New-Item -Path $workDir -ItemType Directory | Out-Null
} else {
    if ((Get-ChildItem $workDir).Count -ge 1) {

        if ($workDirAutoclean -eq $false){
            Write-Host " WARN Temporary folder not empty, target: $workDir"
            $null = Read-Host  " press enter to confirm content deletion"
        }
        Get-ChildItem $workDir -Exclude $($logfile -replace '^.*\\','') | Remove-Item -Force
    }
}
If (!(Test-Path $workDir)) {
    Write-HostRed " ERROR Temporary failed to be created, target: $workDir"
    Write-HostRed " use a different path. Terminating."
    Read-Host ' press any key to terminate'
    Throw

}

# Create log file for recording SCRIPT history
if (!(Test-Path $logfile)){
    New-Item -ItemType File -Path $logfile | Out-Null
} else {
    "`r`n`r`n`r`n" | Out-File -FilePath $logfile -Append -Encoding utf8
}

# Starting with operations
Write-HostYell $((Get-Time) + ' HARDENING STARTED')
$starttime = (Get-Date).ToString("yyyyMMdd_HHmmss") -replace '_(\d{2})(\d{2})(\d{2})', '_$1h$2m$3s'
$problemcnt = 0
        
# Identify all SAFEQ services based on the service name or description
Write-HostWhit $((Get-Time) + ' Locating installation directories')
$ServiceList = @()
$ServiceList += Get-ChildItem -Path HKLM:\SYSTEM\CurrentControlSet\Services | Get-ItemProperty | `
    ? {($_.PSChildName -match 'YSoft.*|YSQ.*' -or $_.DisplayName -match 'YSoft.*|YSQ.*')} | `
    ? {$_.PSPath -notmatch 'YSoftEtcd|YSoftSQ-LDAP|YSoftSafeQLDAPReplicator|YSoftSafeQCMLDBS|YSoftWeb|YSoftPGSQL|YSoftIms'}     
 
# Find the root directory for each service
ForEach ($Service in $ServiceList) {
    $tmp = ($Service.ImagePath -replace '(?<=\.exe).+', '').Trim('`"')
    $tmp = $tmp.Substring(0,$tmp.LastIndexOf('\')) -Replace ('\\?bin\\?','') -Replace ('\\?tomcat\\?','') -Replace ('\\Service\\?','') -Replace ('PGSQL','PGSQL-data') -replace ('\\procrun','')
    $Service | Add-Member -MemberType NoteProperty -Name Path -Value $tmp
}
 
# Exclude services where path does not exist on the filesystem
$FinServiceList = @()
ForEach ($Service in $ServiceList){
    If (Test-Path $Service.Path) { $FinServiceList += $Service }
}

# Check that least one of required services exists
If (!($FinServiceList.PSChildName -match ('YSoftSQ-SPOC|YSoftSQ-Management'))){
    Write-HostRed $((Get-Time) + ' YSoftSQ-SPOC or YSoftSQ-Management service not found. At least one of them is required. Terminating.')
    Read-Host ' press any key to terminate'
    Throw
}

# Detect keytool location
$mngmtdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-Management'}).Path
$spocdir  = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-SPOC'}).Path

$keytoolPaths = @()
if (-not [string]::IsNullOrEmpty($mngmtdir)) {$keytoolPaths += $mngmtdir}
if (-not [string]::IsNullOrEmpty($spocdir))  {$keytoolPaths += $spocdir}

foreach ($path in $keytoolPaths){
    $keytool = $path + '\java\bin\keytool.exe'

    if (Test-Path -Path $keytool) {
        # keytool found, no more searching needed, exit loop
        break
    }
    elseif ($path -eq $keytoolPaths[-1]){
        Write-HostRed $((Get-Time) + ' ERROR Keytool.exe not found, make sure it exists and then run the script again.')

        $expectedLocations = if ($keytoolPaths.Count -gt 1) { $($keytoolPaths -join ' |OR| ') } else { $keytoolPaths }
        Write-HostRed " expected location: $expectedLocations"
        Read-Host ' press any key to terminate'
        Throw
    }
}


# Generate new cert or copy customer's JKS to $workdir
$secfile = $($workDir+("\$($env:computername)_hardening.jks"))
$secalias = 'SAFEQhardening' + '_' + $starttime
$secpass = Generate-RandomString
$secmeta = $($workDir+('\readme.txt'))

if ($generatenewcert -eq $true){
    # Generating key
    Write-HostWhit $((Get-Time) + ' Generating new key/certificate')
    Write-HostGray " pre-configured DN: $certDN"

    $keytoolres = & $keytool -genkeypair -keyalg RSA -keysize 2048 -alias $secalias -keystore $secfile -validity $keyvalidity -keypass $secpass -storepass $secpass -dname $certDN -noprompt 2>$null
} else {
    # Export customer's JKS to temporary JKS
    Write-HostWhit $((Get-Time) + " Exporting customer's JKS to `$workdir")
    $keytoolres = & $keytool -importkeystore -srckeystore $preconfiguredJKS -srcstorepass $preconfiguredJKSpass -srckeypass $preconfiguredJKSpass -srcalias $preconfiguredJKSalias -destkeystore $secfile -deststorepass $secpass -destkeypass $secpass -destalias $secalias -noprompt  2>$null
    if ($LASTEXITCODE -eq 1) {
        Write-HostRed " ERROR script may not continue."
        Write-HostRed " $keytoolres"
        Read-Host ' press any key to terminate'
        Throw
    } else {
        Write-HostGray ' export successful'
    }
}

# Extract new cert details from JKS
$keytoolres = & $keytool -list -v -keystore $secfile -storepass $secpass -alias $secalias -noprompt  2>$null
if ($LASTEXITCODE -eq 1) {
	Write-HostRed " ERROR script may not continue."
	Write-HostRed " $keytoolres"
	Read-Host ' press any key to terminate'
	Throw
}

# Get SHA1 thumbprint
$thumbprint = $keytoolres | Select-String -Pattern "SHA1:" | ForEach-Object { $_ -replace '\s+|:|-|SHA1', '' }

if ($generatenewcert -eq $false) {
    # If JKS was provided by customer, extract SAN details for later verification
    $preconfiguredJKSSANnames = $keytoolres -match 'DNSName:\s+([^`r`n]+)' | ForEach-Object { $_ -replace '^.*:\s' }
    $preconfiguredJKSSANips = $keytoolres -match 'IPAddress:\s+([^`r`n]+)' | ForEach-Object { $_ -replace '^.*:\s' }

    $preconfiguredJKSSAN = @()
    $preconfiguredJKSSAN += $preconfiguredJKSSANnames
    $preconfiguredJKSSAN += $preconfiguredJKSSANips
}

# Write details of new certificate in metadata file and UI
$detailsFormat = @"
Details of new certificate in: $secfile
Key Password: $secpass
Store Password: $secpass
Key Alias: $secalias
moreinfotobeaddedlater

"@

$detailsFormat | Out-File $secmeta -Force

Write-HostGray ' details of new certificate'
Write-HostGray "   path: $secfile"
Write-HostGray "   keypass and storepass: $secpass"
Write-HostGray "   details in: $secmeta"


# Detect local addresses of this server
#  intentionally outside of autodetection and key generating section
#  the same properties used later in the script for TS config 
#  having it outside is optimal for support of for customer's keystore instead of generating a new one

# Get FQDN of the local machine
$localFQDN = [System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName
# Get all IPv4 addresses (excluding localhost)
$localIPv4s = [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME) | Where-Object { $_.AddressFamily -eq 'InterNetwork' -and $_.IPAddressToString -ne '127.0.0.1' } | ForEach-Object { $_.IPAddressToString }
# Merge both together (treat them as arrays)
$localAddr = @($localFQDN) + @($localIPv4s)


# Ensure that SAN is aligned with at least one local address
#  generating a CSR or utilizing a customer's JKS without a matching local address could lead to inconsistencies and certificate mismatch errors
Write-HostGray " ensuring that SAN is aligned with at least one local address"

if ($generatenewcert -eq $true) {

    if ($certAddr -eq 'autodetect'){
        $certAddr = $localAddr
        Write-HostGray " auto-detected addresses: $($certAddr -join ',')"

    } else {
        Write-HostGray " pre-configured addresses: $($certAddr -join ',')"

        # Terminate if wildcard is defined by user in list of addresses
        foreach ($addr in $certAddr) {
            if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($addr)) {
                Write-HostRed " ERROR `$certAddr contains a wildcard, this is not supported by Keytool."
                Write-HostRed " Generate a certificate by yourself e.g. in Keystore Explorer and set the script to use it. Terminating."
                Read-Host ' Press any key to terminate'
                Throw "Wildcard in `$certAddr is not supported."
            }
        }
    }    
} 
else {
    $certAddr = $preconfiguredJKSSAN
    Write-HostGray " customer's JKS addresses: $($certAddr -join ',')"
}

$compareExact = Compare-Object @($localAddr) @($certAddr) -IncludeEqual -ExcludeDifferent
$compareWildcard = $localAddr | Where-Object {
    $addr = $_
    $wildcardPattern = ($certAddr -replace '\*', '.*') -join '|'
    $addr -match $wildcardPattern
}
if (!$compare1 -and !$compareWildcard) {
    Write-HostGray " local server addresses: $($localAddr -join ',')"
    Write-HostRed " ERROR Found 0 match between local and pre-configured addresses, such configuration is not supported. Terminating.".
    Read-Host ' press any key to terminate'
    Throw
}



# Generate CSR for autogenerated cert
if ($generatenewcert -eq $true) {
    Write-HostWhit $((Get-Time) + ' Generating Certificate Signing Request (CSR)')
    $CSRfile = $secfile -replace '.jks$','.csr'

    $CSRsan = 'SAN=' + $(($certAddr | ForEach-Object {
        if ($_ -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') {
            "ip:$_"
        } else {
            "dn:$_"
        }
    }) -join ',')
    Write-HostGray " keytool input: $CSRsan"

    $keytoolres = & $keytool -certreq -keystore $secfile -alias $secalias -keyalg RSA -storepass $secpass -file $CSRfile -ext $CSRsan -ext "keyUsage=digitalSignature,keyEncipherment,nonRepudiation" -ext "extendedKeyUsage=serverAuth,clientAuth" -ext "basicConstraints:Critical=CA:false" -noprompt 2>$null
    "`r`nDetails of: $CSRfile `r`nthis is CSR that needs to be signed by CA`r`next $CSRsan" | Out-File $secmeta -Append -Force

    Write-HostGray ' new CSR generated'
    Write-HostGray "   path: $CSRfile"
    Write-HostGray "   details in: $secmeta"


    # Waiting for admin to sign CSR
    $secCERfile = $secfile -replace '.jks$','.cer'

    Write-HostPink "$((Get-Time) + ' User action required to sign CSR by CA!')"
    Write-HostWhit " 1. Sign this CSR with your CA: $CSRfile"
    Write-HostWhit " 2. The signing process will generate a new file"
    Write-HostGray "    Expected certificate format: X.509 in PEM encoding"
    Write-HostGray "    Expected certificate length: Entire Chain (CA cert and Signed cert)"
    Write-HostWhit " 3. Name the new file: $($secCERfile -replace '^.*\\')"
    Write-HostWhit " 4. Copy the new file to $workDir"
    Write-HostWhit "    The script will proceed automatically after you complete the steps above..."
    do {
        Start-Sleep -Seconds 3
    } until (Test-Path $secCERfile)


    # Process signed CSR
    # check content validity
    Write-HostWhit $((Get-Time) + ' Processing signed certificate')
    $pattern = '(?ms)(-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)'
    $certs = ( $(Get-Content -Path $secCERfile -Raw) | Select-String -Pattern $pattern -AllMatches).Matches.Value

    if ($certs.Count -eq 0){
        Write-HostRed " ERROR $($secCERfile -replace '^.*\\','') not in PEM or contains 0 certificates. Terminating.".
        Read-Host ' press any key to terminate'
        Throw
    }

    if ($certs.Count -eq 1){
        Write-HostRed " ERROR $($secCERfile -replace '^.*\\','') contains 1 certificate only, it must contain whole chain (signed certificate and CA certificate). Terminating.".
        Read-Host ' press any key to terminate'
        Throw
    }

    # import signed cert to JKS
    Write-HostGray ' importing signed certificate back to keystore'
    $keytoolres = & $keytool -importcert -keystore $secfile -file $secCERfile -alias $secalias -storepass $secpass -noprompt  2>$null
    if ($LASTEXITCODE -eq 0) {
        Write-HostGray ' import successful'
    } else {
        Write-HostRed $keytoolres
        Read-Host ' press any key to terminate'
        Throw
    }
}

# store CA cert to external file for later use and extend $secmeta with info about details of signed cert and CA
# NOTE intentionally reading content from JKS, this will later on allow support for custom certificate of customer (instead of generating new cert every time)
$CAfile = $($workDir+('\CA.cer'))
$tempfile = $($workDir+('\tmp.tmp'))

# create temporary file with all certs from JKS
$keytoolres = (& $keytool -list -keystore $secfile -rfc -file $tempfile -storepass $secpass -alias $secalias 2>$null) -join "`n"
if ($LASTEXITCODE -eq 1) {
	Write-HostRed " ERROR script may not continue."
	Write-HostRed " $keytoolres"
	Read-Host ' press any key to terminate'
	Throw
}
$pattern = '(?ms)(-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)'
$certs = ( $keytoolres | Select-String -Pattern $pattern -AllMatches).Matches.Value

# loop through temporary file obtain full certificate details
$certmeta = @()
$secthumbprint = $null
foreach ($cert in $certs) {
    $cert -replace '\r\n', [System.Environment]::NewLine | Set-Content -Path $tempfile -Force
    $certdetails = & $keytool -printcert -file $tempfile 2>$null

    # keep physical copy of the last CA cert (will be used for truststore import); the last CA cert is expected to be Root CA ( https://www.digicert.com/kb/ssl-support/pem-ssl-creation.htm )
    # keep thumbprint of first non-CA in variable $secthumbprint (will be set in config files)
    if ($certdetails -match 'CA:true') {
        Move-Item $tempfile -Destination $CAfile -Force
        Write-HostGray " CA cert exported to $CAfile"
        $CAthumbprint = $certdetails -match '\bSHA1:' -replace '^.*SHA1:\s+|:'
        $certmeta += '---CA---'
    } elseif ($secthumbprint -eq $null) {
        Remove-Item $tempfile -Force
        $secthumbprint = $certdetails -match '\bSHA1:' -replace '^.*SHA1:\s+|:'
        $certmeta += '---cer---'
    } else {
        Remove-Item $tempfile -Force
        $certmeta += '---cer---'
    }

    # read basic details about the certificate metadata
    $certmeta += $certdetails -split "`n" | ForEach-Object {
        if ($_ -match '^\bOwner:|\bIssuer:|\bSerial number:|\bValid from:|\bSHA1:|\bSignature algorithm name:|\bSubject Public Key Algorithm') {
            if ($_ -match '\bSHA1:') {
                $_ -replace ':|\s' -replace 'SHA1','SHA1thumbprint: '
            } else {
                $_
            }
        }

    }

    # if this is the last cert in loop, make sure CA export succeeded (CA certificate is required for further steps)
    if ($cert -eq $certs[-1]) {
        if (-not(Test-Path $CAfile)){
            Write-HostRed $((Get-Time) + " ERROR CA certificate not found in $($secCERfile -replace '^.*\\','') . Terminating.")
            Read-Host ' press any key to terminate'
            Throw # CA not found and no more checks to be made
        }
    }
}

# update metadata file
(Get-Content $secmeta -Raw) -replace 'moreinfotobeaddedlater',$($certmeta -join "`n") | Out-File $secmeta -Force


# convert new JKS to p12
Write-HostWhit $((Get-Time) + ' Converting JKS keystore to p12')
$secp12file = $secfile -replace '.jks$','.p12'

$keytoolres = & $keytool -importkeystore -srckeystore $secfile -srcstoretype JKS -srcstorepass $secpass -srckeypass $secpass -srcalias $secalias -destkeystore $secp12file -deststorepass $secpass -deststoretype PKCS12  -noprompt  2>$null
if ($LASTEXITCODE -eq 0) {
    Write-HostGray ' conversion successful'
} else {
    Write-HostRed $keytoolres
    Read-Host ' press any key to terminate'
    Throw
}
"`r`nDetails of: $secp12file `r`nthis is PCKS12 conversion of $secfile `r`nit uses the same alias and password" | Out-File $secmeta -Append -Force
Write-HostGray " path: $secp12file"
Write-HostGray " details in: $secmeta"




# CONFIGURING MNMGT
Write-HostYell $((Get-Time) + ' --- Configuring Management to use new certificate ---')

if ([string]::IsNullOrEmpty($MNGMTdir)) {
    Write-HostGray ' skipped, MNGMT not installed'
} 
else {

    # Generate new truststore and keystore for MNGMT communicator 
    # (original ssl-truststore and ssl-keystore are hardcoded and better to avoid their modification to prevent overwrite on Build update)
    Write-HostWhit $((Get-Time) + ' Generating new keystore and truststore for Management communicator')
    $MNGMTtruststorefile = $MNGMTdir+'\conf\communicator_truststore' -replace '/','\'
    $MNGMTtruststorepass = $secpass
    $MNGMTkeystorefile = $MNGMTdir+'\conf\communicator_keystore' -replace '/','\'
    $MNGMTkeystorepass = $secpass

    # Create MNGMT communicator truststore with CA
    Create-Backup $MNGMTtruststorefile
    Remove-Item $MNGMTtruststorefile -Force -ErrorAction SilentlyContinue
    $keytoolres = & $keytool -printcert -file $CAfile -noprompt  2>$null
    $CAserial = $($keytoolres | ? {$_ -match 'Serial\snumber'}) -replace '^.*:\s','SN_'
    $CAalias  = 'SAFEQhardeningCA'+'_'+$CAserial
    $keytoolres = & $keytool -importcert -keystore $MNGMTtruststorefile -file $CAfile -alias $CAalias -storepass $MNGMTtruststorepass -noprompt  2>$null
    if ($LASTEXITCODE -eq 0) {
        Write-HostGray " truststore created: $MNGMTtruststorefile"
    } else {
        Write-HostRed $keytoolres
        Read-Host ' press any key to terminate'
        Throw
    }

    # Use copy of newly generated keystore as MNGMT communicator keystore
    Create-Backup $MNGMTkeystorefile
    try {
        Copy-Item -Path $secfile -Destination $MNGMTkeystorefile -Force -ErrorAction Stop
        if ($?) {
            Write-HostGray " truststore created: $MNGMTkeystorefile"
        }
    }
    catch {
        Write-HostRed "ERROR copying $secfile to $MNGMTkeystorefile"
        Write-HostRed " $_"
        Write-HostRed " resolve the error, then run the script again."
        Read-Host ' press any key to terminate'
        Throw
    }


    # 01 Reconfigure MNGMT to use secure communication channel with SPOCs
    Write-HostWhit $((Get-Time) + ' Altering communicator.conf')
    $MNGMTcomconf = $MNGMTdir+'\conf\communicator.conf'

    $newConfig = "
        secureCommunicationEnabled=true
        truststoreFile=$($MNGMTtruststorefile -replace '\\','/') 
        truststorePassword=$($MNGMTtruststorepass)
        keyStoreFile=$($MNGMTkeystorefile -replace '\\','/')
        keystorePassword=$($MNGMTkeystorepass)
        certificateAlias=$($secalias)
        sslProtocol=TLS
        clientAuthenticationRequired=true
    "
    Update-RAWconf $MNGMTcomconf $newConfig

    # 02 Reconfigure MNGMT to use secure communication channel with other MNGMT cluster members
    Write-HostWhit $((Get-Time) + ' Altering clusterCommunicator.conf')
    $MNGMTclusconf = $MNGMTdir+'\conf\clusterCommunicator.conf'

    $newConfig = "
        secureCommunicationEnabled=true
        truststoreFile=$($MNGMTtruststorefile -replace '\\','/') 
        truststorePassword=$($MNGMTtruststorepass)
        keyStoreFile=$($MNGMTkeystorefile -replace '\\','/')
        keystorePassword=$($MNGMTkeystorepass)
        certificateAlias=$($secalias)
        sslProtocol=TLS
        clientAuthenticationRequired=true
    "
    Update-RAWconf $MNGMTclusconf $newConfig



    # Set MNGMT web interface to use new cert
    Write-HostWhit $((Get-Time) + ' Configuring Management interface with new certificate')

    # read keystore/truststore location and password from MNGMT XML
    $MNGMTconf = $MNGMTdir + '\tomcat\conf\server.xml'
    $SectionPath = 'Server.Service.Connector'
    Write-HostGray " reading keystore/truststore details from $MNGMTconf"
    $XMLstoredetail = Read-TomcatXMLdetails -XmlPath $MNGMTconf -SectionPath $SectionPath -LookupParameters 'port','certificateKeystoreFile','certificateKeystorePassword','certificateKeystoreType'

    if ($XMLstoredetail -eq $null){
        # parameter check, skip MNGMT reconfiguration when some parameters missing (possible cause is different xml syntax for older Apache Tomcat)
        Write-HostRed " reconfiguration of MNGMT web interface certificate skipped"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set MNGMT per the documentation manually"
        $problemcnt = $problemcnt + 1
    } 
    elseif ($XMLstoredetail.certificateKeystoreType -ne 'JKS') {
        # only JKS format supported now
        Write-HostRed " reconfiguration of MNGMT web interface certificate skipped because parameter certificateKeystoreType is not JKS"
        Write-HostRed " once script finishes, alter XML and run the script again or set MNGMT per the documentation manually"
        $problemcnt = $problemcnt + 1        
    }
    else {
        # feed content of parameters to variables
        $MNGMTport = $XMLstoredetail.port
        $MNGMTkeystorefile = $XMLstoredetail.certificateKeystoreFile -replace '/','\' -replace '\.\.\\conf',"$($MNGMTdir)\conf"
        $MNGMTkeystorepass = $XMLstoredetail.certificateKeystorePassword


        # import new cert to MNGMT keystore
        Write-HostGray ' importing new cert to MNGMT keystore'
        Create-Backup $MNGMTkeystorefile
        $keytoolres = & $keytool -importkeystore -srckeystore $secfile -srcstorepass $secpass -srckeypass $secpass -srcalias $secalias -destkeystore $MNGMTkeystorefile -deststorepass $MNGMTkeystorepass -destkeypass $MNGMTkeystorepass -noprompt  2>$null
        if ($LASTEXITCODE -eq 0) {
            Write-HostGray ' import successful'
        } else {
            Write-HostRed $keytoolres
            Read-Host ' press any key to terminate'
            Throw
        }

        # reconfigure MNGMT Web Interface to use the new certificate
        Write-HostWhit $((Get-Time) + ' Altering server.xml to replace website certificate ')
        $KeyValuePairs = @{
            "certificateKeyAlias" = "$($secalias)"
            "certificateKeyPassword" = "$($MNGMTkeystorepass)"
        }
        Update-XmlSection -XmlPath $MNGMTconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs -TomcatConnectorPort $MNGMTport
    }
}




# CONFIGURING SPOC
Write-HostYell $((Get-Time) + ' --- Configuring SPOC to use new certificate ---')

if ([string]::IsNullOrEmpty($spocdir)) {
    Write-HostGray ' skipped, SPOC not installed'
} 
else {

    # Get keystore/truststore details from spoc wrapper
    Write-HostWhit $((Get-Time) + ' Importing CA cert to SPOC truststore')
    $spocwrapper = $spocdir+'\bin\wrapper.conf'
    $spoctruststorefile = $(Get-Content $spocwrapper | ? {$_ -match 'trustStore='}) -replace '^.*=','' -replace '/','\'
    $spoctruststorepass = $(Get-Content $spocwrapper | ? {$_ -match 'trustStorePassword='}) -replace '^.*=',''
    $spockeystorefile = $(Get-Content $spocwrapper | ? {$_ -match 'keyStore='}) -replace '^.*=','' -replace '/','\'
    $spockeystorepass = $(Get-Content $spocwrapper | ? {$_ -match 'keyStorePassword='}) -replace '^.*=',''

    # Import CA to spoc truststore
    Create-Backup $spoctruststorefile
    $keytoolres = & $keytool -printcert -file $CAfile -noprompt  2>$null
    $CAserial = $($keytoolres | ? {$_ -match 'Serial\snumber'}) -replace '^.*:\s','SN_'
    $CAalias  = 'SAFEQhardeningCA'+'_'+$CAserial
    $keytoolres = & $keytool -importcert -keystore $spoctruststorefile -file $CAfile -alias $CAalias -storepass $spoctruststorepass -noprompt  2>$null
    if ($LASTEXITCODE -eq 0) {
        Write-HostGray ' import successful'
    } elseif ($keytoolres -match 'already exists'){
        Write-HostGray ' import skipped, CA with the same Serial Number already present'
    } else {
        Write-HostRed $keytoolres
        Read-Host ' press any key to terminate'
        Throw
    }

    # Import new cert to SPOC keystore
    Write-HostWhit $((Get-Time) + ' Importing new cert to SPOC keystore')
    Create-Backup $spockeystorefile
    $keytoolres = & $keytool -importkeystore -srckeystore $secfile -srcstorepass $secpass -srckeypass $secpass -srcalias $secalias -destkeystore $spockeystorefile -deststorepass $spockeystorepass -destkeypass $spockeystorepass -noprompt  2>$null
    if ($LASTEXITCODE -eq 0) {
        Write-HostGray ' import successful'
    } else {
        Write-HostRed $keytoolres
        Read-Host ' press any key to terminate'
        Throw
    }

    # 01 Reconfigure SPOC to use secure communication channel with subcomponents (EUI, FSP...)
    Write-HostWhit $((Get-Time) + ' Altering ymq_config.properties')
    $spocymqconf = $spocdir+'\conf\ymq_config.properties'

    $newConfig = "
        keyStorePath=$($spockeystorefile -replace '\\','/')
        keyStorePassword=$($spockeystorepass)
        keyStoreAlias=$($secalias)
        keyStoreAliasPassword=$($spockeystorepass)
        trustStorePath=$($spoctruststorefile -replace '\\','/') 
        trustStorePassword=$($spoctruststorepass)
        ellipticCurve=secp256r1
        certificateValidationMode=VALIDATE_CERTIFICATE
    "
    
    Update-RAWconf $spocymqconf $newConfig


    # 02 Reconfigure SPOC to use secure communication channel to Management
    Write-HostWhit $((Get-Time) + ' Altering communicator.conf')
    $spoccomconf = $spocdir+'\conf\communicator.conf'

    $newConfig = "
        secureCommunicationEnabled=true
        truststoreFile=$($spoctruststorefile -replace '\\','/') 
        truststorePassword=$($spoctruststorepass)
        keyStoreFile=$($spockeystorefile -replace '\\','/')
        keystorePassword=$($spockeystorepass)
        certificateAlias=$($secalias)
        sslProtocol=TLS
        clientAuthenticationRequired=true
    "

    Update-RAWconf $spoccomconf $newConfig


    

    # CONFIGURING TS

    Write-HostYell $((Get-Time) + ' --- Configuring TS ---')
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    $TSdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-TS'}).Path
    $TSconf = $TSdir + '\terminalserver.exe.config'
    $SectionPath = 'configuration.appSettings'

    Write-HostWhit $((Get-Time) + ' Altering terminalserver.exe.config')

    [xml]$TSxml = Get-Content -Path $TSconf
    $appSettings = $TSxml.configuration.appSettings

    # Find suitable value, FQDN preferred over IP, selected value must match SANs of newly used certificate
    $newwpsBaseAddress = $(
        if ($script:secwincertstoreSAN -match $localFQDN) {
            'https://' + $localFQDN
        } else {
            foreach ($ipv4 in $localIPv4s){
                if ($script:secwincertstoreSAN -match $ipv4) {
                    'https://' + $ipv4
                    break
                }
            }
        }
        # No failure detection here, match between local address and certificate SAN was performed earlier in the script
    )

    # Check current value
    $oldwpsBaseAddress = ($appSettings.SelectSingleNode('add[@key="wpsBaseAddress"]')).value
    # Define new value, retain existing port number
    $newwpsBaseAddress = "$($oldwpsBaseAddress)" -replace '^.*:',"$($newwpsBaseAddress):"

    if ($replaceTScert -eq $true) {
        $KeyValuePairs = @{
            "wpsBaseAddress" = "$($newwpsBaseAddress)"
            "dsCertificateSource" = "WindowsCertStore"
            "dsCertificateStore" = "my"
            "dsCertificateStoreIdentifier" = "$($secthumbprint)"
            "ymqEncryption" = "Aes128"
        }    
    }
    else {
        $KeyValuePairs = @{
            "wpsBaseAddress" = "$($newwpsBaseAddress)"
        }
    }
    Update-XmlSection -XmlPath $TSconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs



    # CONFIGURING EUI

    Write-HostYell $((Get-Time) + ' --- Configuring EUI ---')
    Write-HostWhit $((Get-Time) + ' Importing CA cert to EUI truststore')

    # Read keystore/truststore location and password from EUI XML
    $EUIdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-EUI'}).Path
    $EUIconf = $EUIdir + '\conf\server.xml'
    $SectionPath = 'Server.Service.Connector'
    Write-HostGray " reading keystore/truststore details from $EUIconf"
    $XMLstoredetail = Read-TomcatXMLdetails -XmlPath $EUIconf -SectionPath $SectionPath -LookupParameters 'port','truststoreFile','truststorePassword','truststoreType','certificateKeystoreFile','certificateKeystorePassword','certificateKeystoreType'

    if ($XMLstoredetail -eq $null){
        # Parameter check, skip EUI reconfiguration when some parameters missing (possible cause is different xml syntax for older Apache Tomcat)
        Write-HostRed " reconfiguration of EUI skipped"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set EUI per the documentation manually"
        $problemcnt = $problemcnt + 1
    }
    elseif ($XMLstoredetail.certificateKeystoreType -ne 'JKS') {
        # only JKS format supported now
        Write-HostRed " reconfiguration of EUI certificate skipped because parameter certificateKeystoreType is not JKS"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set EUI per the documentation manually"
        $problemcnt = $problemcnt + 1        
    }
    elseif ($XMLstoredetail.truststoreType -ne 'JKS') {
        # only JKS format supported now
        Write-HostRed " reconfiguration of EUI certificate skipped because parameter truststoreType is not JKS"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set EUI per the documentation manually"
        $problemcnt = $problemcnt + 1        
    }    
    else {
        # Feed content of parameters to variables
        $EUIport = $XMLstoredetail.port
        $EUItruststorefile = $XMLstoredetail.truststoreFile -replace '/','\' -replace '\$\{catalina.base\}',"$($EUIdir)"
        $EUItruststorepass = $XMLstoredetail.truststorePassword
        $EUIkeystorefile = $XMLstoredetail.certificateKeystoreFile -replace '/','\' -replace '\$\{catalina.base\}',"$($EUIdir)"
        $EUIkeystorepass = $XMLstoredetail.certificateKeystorePassword

        # Insert CA to EUI truststore 
        Create-Backup $EUItruststorefile
        $keytoolres = & $keytool -importcert -keystore $EUItruststorefile -file $CAfile -alias $CAalias -storepass $EUItruststorepass -noprompt  2>$null
        if ($LASTEXITCODE -eq 0) {
            Write-HostGray ' import successful'
        } elseif ($keytoolres -match 'already exists'){
            Write-HostGray ' import skipped, CA with the same Serial Number already present'
        } else {
            Write-HostRed $keytoolres
            Read-Host ' press any key to terminate'
            Throw
        }

        # Import new cert to EUI keystore
        Write-HostWhit $((Get-Time) + ' Importing new cert to EUI keystore')
        Create-Backup $EUIkeystorefile
        $keytoolres = & $keytool -importkeystore -srckeystore $secfile -srcstorepass $secpass -srckeypass $secpass -srcalias $secalias -destkeystore $EUIkeystorefile -deststorepass $EUIkeystorepass -destkeypass $EUIkeystorepass -noprompt  2>$null
        if ($LASTEXITCODE -eq 0) {
            Write-HostGray ' import successful'
        } else {
            Write-HostRed $keytoolres
            Read-Host ' press any key to terminate'
            Throw
        }

        # Reconfigure EUI for secure communication
        Write-HostWhit $((Get-Time) + ' Altering environment-configuration.properties')
        $EUIenvprop = $EUIdir+'\ui-conf\environment-configuration.properties'

        $newConfig = "
            messagingContext.secureMechanism=secured-messaging-context
            messagingContext.keyStorePath=$($EUIkeystorefile -replace '\\','/')
            messagingContext.keyStorePassword=$($EUIkeystorepass)
            messagingContext.keyStoreAlias=$($secalias)
            messagingContext.keyStoreAliasPassword=$($EUIkeystorepass)
            messagingContext.trustStorePath=$($EUItruststorefile -replace '\\','/')
            messagingContext.trustStorePassword=$($EUItruststorepass)
            messagingContext.ellipticCurve=secp256r1
            messagingContext.encryptionKeyLength=16
        "
        Update-RAWconf $EUIenvprop $newConfig

        # Reconfigure EUI Web Interface to use the new certificate
        Write-HostWhit $((Get-Time) + ' Altering server.xml to replace website certificate ')
        $KeyValuePairs = @{
            "certificateKeyAlias" = "$($secalias)"
        }
        Update-XmlSection -XmlPath $EUIconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs -TomcatConnectorPort $EUIport
    }
}



# CONFIGURING FSP

Write-HostYell $((Get-Time) + ' --- Configuring FSP ---')

$FSPdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-FSP'}).Path
$FSPconf = $FSPdir + '\Service\spooler.config'

if ([string]::IsNullOrEmpty($FSPdir)) {
    Write-HostGray ' skipped, FSP not installed'
} 
else {
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    # Reconfigure FSP for secure communication, then replace existing cert for job reception by CA signed cert (by default it uses self-signed)
    Write-HostWhit $((Get-Time) + ' Altering spooler.config')

    Create-Backup $FSPconf
    $originalConfig = Get-Content -Path $FSPconf -Raw | ConvertFrom-Json

    # Add or update required properties
    $KeyValuePairs = @{
        "CertificateThumbprint" = "$($secthumbprint)"
        "encryption" = "Aes128"
        "dsCertificateSource" = "windowscertstore"
        "dsCertificateStore" = "my"
        "dsCertificateStoreIdentifier" = "$($secthumbprint)"
    }

    $KeyValuePairs.Keys | ForEach-Object {
        $originalConfig | Add-Member -NotePropertyName $_ -NotePropertyValue $KeyValuePairs[$_] -Force
    }

    $originalConfig | ConvertTo-Json | Set-Content -Path $FSPconf
    Write-HostGray ' altering finished'
}




# CONFIGURING MPS

Write-HostYell $((Get-Time) + ' --- Configuring MPS ---')

$MPSdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-MPS'}).Path
$MPSconf = $MPSdir + '\Service\conf\mps.config'

# skip if MPS not installed
if ([string]::IsNullOrEmpty($MPSdir)) {
    Write-HostGray ' skipped, MPS not installed'
} 
else {

    Write-HostWhit $((Get-Time) + ' Altering mps.config')
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    # create config backup (other components have it done in Update-XmlSection, MPS is exception because it is updated twice and doing two backups is nonsense)
    Create-Backup $MPSconf
    
    # section related to communication channel towards secured SPOC
    $SectionPath = 'mps.communicator'
    $KeyValuePairs = @{
        "certificateThumbprint" = "$($secthumbprint)"
        "encryption" = "Aes128"
    }
    Update-XmlSection -XmlPath $MPSconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs -SkipBackup

    # section related to newly CA singned certificate of FSP for job reception
    $SectionPath = 'mps.http'
    $KeyValuePairs = @{
        "ignoreCertificateChainErrors" = "false"
    }
    Update-XmlSection -XmlPath $MPSconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs -SkipBackup
}
    



# CONFIGURING WPS

Write-HostYell $((Get-Time) + ' --- Configuring WPS ---')
$WPSdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-WPS'}).Path
$WPSconf = $WPSdir + '\WpsService.exe.config'
$SectionPath = 'configuration.appSettings'

# skip if WPS not installed
if ([string]::IsNullOrEmpty($WPSdir)) {
        Write-HostGray ' skipped, WPS not installed'
} 
else {
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    # Read WPS configuration
    Write-HostWhit $((Get-Time) + ' Altering WpsService.exe.config')

    $KeyValuePairs = @{
        "certificateThumbprint" = "$($secthumbprint)"
        "ymqEncryption" = "Aes128"
    }
    Update-XmlSection -XmlPath $WPSconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs
}




# CONFIGURING MIG

Write-HostYell $((Get-Time) + ' --- Configuring MIG ---')

$MIGdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-MIG'}).Path
$MIGconf = $MIGdir + '\bin\MigService.exe.config'
$SectionPath = 'configuration.appSettings'

# skip if MIG not installed
if ([string]::IsNullOrEmpty($MIGdir)) {
    Write-HostGray ' skipped, MIG not installed'
} 
else {
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    Write-HostWhit $((Get-Time) + ' Altering MigService.exe.config')

    # Configure MIG to use new cert for secure communication with other SAFEQ components such as SPOC
    $KeyValuePairs = @{
        "certificate-hash" = "$($secthumbprint)"
        "validate-spooler-certificate" = "T"
    }
    Update-XmlSection -XmlPath $MIGconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs


    # Configure MIG to use new cert for ipps clients
    if ($replaceMIGcert -eq $true){

        Write-HostWhit $((Get-Time) + ' Replacing ipps client facing certificate in MIG')

        # Detect MIG port
        [xml]$migconfig = Get-Content -Path $MIGconf
        $ippPort = $migconfig.configuration.appSettings.add | Where-Object { $_.key -eq 'ipp-port' } | Select-Object -ExpandProperty value

        # port for MIG found in configuration file, script can continue
        if ($ippPort -ne $null) {
            Write-HostGray " ipp-port value found in config file: $ippPort"

            # renaming autogenerated PFX on filesystem (file is no longer going to be used and physical copy in MIG dir is not required)
            $obsoleteMIGpfx = $MIGdir + '\support\MIG_server_certificate.pfx'
            $obsoleteMIGpfxrename = $($obsoleteMIGpfx + '.bck' + $starttime)
            if (Test-Path $obsoleteMIGpfx) {
                try {
                    Move-Item -Path $obsoleteMIGpfx -Destination $obsoleteMIGpfxrename -Force -ErrorAction Stop
                    if ($?) {
                        Write-HostGray " obsolete pfx renamed to $obsoleteMIGpfxrename"
                    }
                } catch {
                    Write-HostGray " error renaming obsolete pfx $obsoleteMIGpfx to $obsoleteMIGpfxrename"
                    Write-HostGray " $_"
                    Write-HostRed " you can rename the file manually, file is no longer useful."
                }
            }

            # checking current $ippPort bindings in netsh (neths is what MIG uses for ipps certificate configuration)
            $netshbindings = netsh http show sslcert
            $oldMIGippscert = $netshbindings | ? {$_ -match ":$ippPort"}

            if ($oldMIGippscert.Count -eq 0) {
                Write-HostGray " WARN No bindings for port $ippPort found in the http.sys, script will create one"
            } else {
                Write-HostGray " count of port $ippPort bindings in http.sys before replacement: $($oldMIGippscert.Count)"                

                # trying to load hash of old certificate (may be OS language dependent, only ipv4 supported)
                $bindingsArray = @()
                $regexPattern = 'IP:port\s*:\s*(\d+\.\d+\.\d+\.\d+:\d+)\s*Certificate Hash\s*:\s*([A-Fa-f0-9]+)'
                $matches = [regex]::Matches($netshbindings, $regexPattern)
                foreach ($match in $matches) {
                    $bindingInfo = $match.Groups[1].Value.Split(':')
                    $IPAddress = $bindingInfo[0]
                    $Port = [int]$bindingInfo[1]
                    $CertificateHash = $match.Groups[2].Value
    
                    $binding = [PSCustomObject]@{
                        IPAddress = $IPAddress
                        Port = $Port
                        CertificateHash = $CertificateHash
                    }
                    $bindingsArray += $binding
                }
                $oldMIGippshash = $($bindingsArray | ? {$_.Port -eq $ippPort}).CertificateHash -join ', '

                if ($oldMIGippshash -ne $null) {
                    Write-HostGray " old certificate thumbprint(s): $oldMIGippshash"                                    
                }
            }

            # deleting existing binding for $ippPort, then creating binding for new certificate
            Write-HostGray " replacing port $ippPort certificate in the http.sys"
            if ($oldMIGippscert.Count -gt 0) {
                $netshres =  &"netsh" http delete sslcert ipport=0.0.0.0:$ippPort 2>&1
                Write-HostGray $($netshres -join ' ')
            }
            $netshres = &"netsh" http add sslcert ipport=0.0.0.0:$ippPort certhash=$($secthumbprint) appid=`{214124cd-d05b-4309-9af9-9caa44b2b74a`} 2>&1
            Write-HostGray $($netshres -join ' ')

            $newWebcerts = netsh http show sslcert | ? {$_ -match $secthumbprint }
            Write-HostGray " count of port $ippPort bindings in http.sys after the replacement: $($newWebcerts.Count)"

            # check for successful 
            if ($newWebcerts.Count -lt 0) {
                Write-HostRed " ERROR count of http.sys bindings after the replacement is lower than before the fix"
                Write-HostRed " once script finishes, replace MIG ipps certificate per documentation"
                $problemcnt = $problemcnt + 1
            } elseif ($netshres -match 'error|failed') {
                Write-HostRed " ERROR netsh command error (see above)"
                Write-HostRed " once script finishes, replace MIG ipps certificate per documentation"
                $problemcnt = $problemcnt + 1
            } else {
                Write-HostGray " replacing finished"
            }

        } 
        # port for MIG not defined in config, script will continue but will not alter MIG ipps certificate
        else {
            Write-HostRed " ERROR Found 0 values of ipp-port in $MIGconf ."
            Write-HostRed " once script finishes, replace MIG ipps certificate per documentation or fix configuration file and run script again"
            $problemcnt = $problemcnt + 1
        }
    }
}




# CONFIGURING YPS

Write-HostYell $((Get-Time) + ' --- Configuring Payment System ---')
$YPSdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftPS'}).Path

# skip if YPS not installed
if ([string]::IsNullOrEmpty($YPSdir)) {
    Write-HostGray ' skipped, YPS not installed'
} 
else {
    # Set YPS web interface to use new cert
    Write-HostWhit $((Get-Time) + ' Configuring YPS interface with new certificate')

    # read keystore/truststore location and password from YPS XML
    $YPSconf = $YPSdir + '\conf\server.xml'
    $SectionPath = 'Server.Service.Connector'
    Write-HostGray " reading keystore details from $YPSconf"
    $XMLstoredetail = Read-TomcatXMLdetails -XmlPath $YPSconf -SectionPath $SectionPath -LookupParameters 'port','certificateKeystoreFile','certificateKeystorePassword','certificateKeystoreType'

    if ($XMLstoredetail -eq $null){
        # parameter check, skip YPS reconfiguration when some parameters missing (possible cause is different xml syntax for older Apache Tomcat)
        Write-HostRed " reconfiguration of YPS web interface certificate skipped"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set YPS per the documentation manually"
        $problemcnt = $problemcnt + 1
    }
    elseif ($XMLstoredetail.certificateKeystoreType -ne 'JKS') {
        # only JKS format supported now
        Write-HostRed " reconfiguration of YPS certificate skipped because parameter certificateKeystoreType is not JKS"
        Write-HostRed " once script finishes, alter XML to have all parameters set and run the script again or set YPS per the documentation manually"
        $problemcnt = $problemcnt + 1        
    }    
    else {
        # feed content of parameters to variables
        $YPSport = $XMLstoredetail.port
        $YPSkeystorefile = $XMLstoredetail.certificateKeystoreFile -replace '/','\' -replace '\$\{catalina.base\}',"$($YPSdir)"
        $YPSkeystorepass = $XMLstoredetail.certificateKeystorePassword

        # import new cert to YPS keystore
        Write-HostGray ' importing new cert to YPS keystore'
        Create-Backup $YPSkeystorefile
        $keytoolres = & $keytool -importkeystore -srckeystore $secfile -srcstorepass $secpass -srckeypass $secpass -srcalias $secalias -destkeystore $YPSkeystorefile -deststorepass $YPSkeystorepass -destkeypass $YPSkeystorepass -noprompt  2>$null
        if ($LASTEXITCODE -eq 0) {
            Write-HostGray ' import successful'
        } else {
            Write-HostRed $keytoolres
            Read-Host ' press any key to terminate'
            Throw
        }

        # reconfigure YPS Web Interface to use the new certificate
        Write-HostWhit $((Get-Time) + ' Altering server.xml to replace website certificate ')
        $KeyValuePairs = @{
            "certificateKeyAlias" = "$($secalias)"
        }
        Update-XmlSection -XmlPath $YPSconf -SectionPath $SectionPath -KeyValuePairs $KeyValuePairs -TomcatConnectorPort $YPSport
    }


    # Import CA to YPS truststore, required for Payment Gateway Plugin functionality
    # truststore values taken from environment-configuration.properties (or use defaults)
    Write-HostWhit $((Get-Time) + ' Importing CA cert to YPS truststore')    
    $YPSenvprop = $YPSdir+'\ps-conf\environment-configuration.properties'
    Write-HostGray " reading $YPSenvprop"

    # defaults (used when keys are missing)
    $defaultTruststorePath = $YPSdir+'\ps-conf\truststore.jks'
    $defaultTruststorePass = "bYOARJ741l"

    # helper: read key=value (ignores commented lines starting with #)
    function Get-PropValue {
        param(
            [Parameter(Mandatory)] [string]$Text,
            [Parameter(Mandatory)] [string]$Key
        )

        # (?m) makes ^ and $ work per-line inside a multi-line string
        $pattern = "(?m)^(?!\s*#)\s*$([regex]::Escape($Key))\s*=\s*(.*?)\s*$"
        $m = [regex]::Match($Text, $pattern)
        if ($m.Success) { return $m.Groups[1].Value }
        return $null
    }

    # read file (file always exists with installed YPS)
    $envText = Get-Content -Path $YPSenvprop -Raw -ErrorAction Stop
    $YPStruststorefile = Get-PropValue -Text $envText -Key "truststore.path"
    $YPStruststorepass = Get-PropValue -Text $envText -Key "truststore.password"

    # apply defaults if missing/empty
    if ([string]::IsNullOrWhiteSpace($YPStruststorefile)) { $YPStruststorefile = $defaultTruststorePath; Write-HostGray " truststore.path not set, using default: $YPStruststorefile" } else {Write-HostGray " truststore.path read: $YPStruststorefile"}
    if ([string]::IsNullOrWhiteSpace($YPStruststorepass)) { $YPStruststorepass = $defaultTruststorePass; Write-HostGray " truststore.password not set, using default: $YPStruststorepass" } else {Write-HostGray " truststore.password read: $YPStruststorepass"}
    $YPStruststorefile = $YPStruststorefile -replace '\<payment_home\>',"$($YPSdir)" -replace '\/','\'

    # Insert CA to YPS truststore 
    Create-Backup $YPStruststorefile
    $keytoolres = & $keytool -importcert -keystore $YPStruststorefile -file $CAfile -alias $CAalias -storepass $YPStruststorepass -noprompt  2>$null
    if ($LASTEXITCODE -eq 0) {
        Write-HostGray ' import successful'
    } elseif ($keytoolres -match 'already exists'){
        Write-HostGray ' import skipped, CA with the same Serial Number already present'
    } else {
        Write-HostRed $keytoolres
        Read-Host ' press any key to terminate'
        Throw
    }

}




# CONFIGURING JS

Write-HostYell $((Get-Time) + ' --- Configuring Job Service ---')
$JSdir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-JOB-SERVICE'}).Path

# skip if JS not installed
if ([string]::IsNullOrEmpty($JSdir)) {
    Write-HostGray ' skipped, JS not installed'
} 
else {
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    # Set JS to use new cert for YMQ towards SPOC and replace HTTPS cert
    Write-HostWhit $((Get-Time) + ' Altering local.json')
    $JSconf = $JSdir + '\configuration\local.json'

    $newConfig = '{
        "YmqProxyOptions": {
            "SecurityOptions": {
                "ValidateServerCertificate": true,
                "CertificateStoreName": "My",
                "CertificateStoreLocation": "LocalMachine",
                "CertificateThumbprint": "' + $secthumbprint + '",
                "Encryption": "Aes128"
            }
        }'

    if ($replaceJScert -eq $true){
        # Add a comma to separate the new object from the previous one
        $newConfig += ','
    
        # Add the new object
        $newConfig += '
            "HttpServerOptions": {
                "CertificateOptions": {
                    "Thumbprint": "' + $secthumbprint + '"
                }
            }'
   }

    # Close the final JSON object
    $newConfig += '}'
    $newConfig = $newConfig | ConvertFrom-Json

    Create-Backup $JSconf
    $config = Get-Content $JSconf | ConvertFrom-Json
    $config = JSONAddPropertyRecurse -orig $config -addition $newConfig
    $config = JSONsort $config
    $($config | ConvertTo-Json -Depth 20) -replace '[^\S\r\n]{2,7}',' ' | Out-File $JSconf -Encoding utf8
    Write-HostGray ' altering finished'
}



# CONFIGURING V3 SPOOLER

Write-HostYell $((Get-Time) + ' --- Configuring V3 Spooler ---')
$V3dir = $($FinServiceList | ? {$_.PSChildName -eq 'YSoftSQ-SPOOLER'}).Path

# skip if V3 not installed
if ([string]::IsNullOrEmpty($V3dir)) {
    Write-HostGray ' skipped, V3 spooler not installed'
} 
else {
    # Make sure new cert and CA exists in Windows Certificate Store
    Import-Cert-Windows

    # Set V3 to use new cert on HTTPS
    Write-HostWhit $((Get-Time) + ' Altering local.json')
    $V3conf = $V3dir + '\configuration\local.json'

    if ($replaceJScert -eq $true) {
        $newConfig = '{
            "HttpServerOptions": {
                "CertificateOptions": {
                    "Thumbprint": "' + $secthumbprint + '"
                }
            }
        }'

        $newConfig = $newConfig | ConvertFrom-Json

        Create-Backup $V3conf

        $config = Get-Content $V3conf | ConvertFrom-Json
        $config = JSONAddPropertyRecurse -orig $config -addition $newConfig
        $config = JSONsort $config
        $($config | ConvertTo-Json -Depth 20) -replace '[^\S\r\n]{2,7}',' ' | Out-File $V3conf -Encoding utf8
        Write-HostGray ' altering finished'
    }
    else {
        Write-HostGray ' $replaceJScert is false, no changes made'
    }
    

}




# Finalization part

if ($problemcnt -eq 0) {
    Write-HostYell $((Get-Time) + ' HARDENING FINISHED, all modifications done')
} else {
    Write-HostRed $((Get-Time) + ' HARDENING FINISHED WITH ERROR, some modifications not done')
}

Write-HostWhit ' Additional required steps'
if ($problemcnt -ne 0)  { Write-HostWhit ' 0. See details of error in the screen or in the log and resolve it'}
Write-HostWhit ' 1. Set following in Management Interface > System > Configuration:'
Write-HostWhit '    usePlainCommunicationForOrsSubsystems = Disabled'
Write-HostWhit '    ypsServerCertificateValidationType = Standard'
Write-HostWhit ' 2. Restart all services (mind the proper order for SPOC GROUP)'
Write-HostGray '    PowerShell restart command:'
Write-HostGray "    `$services = 'YSoftSQ*Management','YSoftSQ*SPOC','YSoftSQ*EUI','YSoftSQ*WPS','YSoftSQ*TS','YSoftSQ*FSP','YSoftSQ*MPS','YSoftSQ*MIG','YSof*PS','YSoftSQ*JOB-SERVICE','YSoftSQ*SPOOLER'"
Write-HostGray '    foreach ($service in $services) { Stop-Service -Name $service -Force }'
Write-HostGray '    foreach ($service in $services) { Start-Service -Name $service }'
Write-HostWhit ' 3. Run this script on all the Management and SPOC servers in the environment'
Write-HostWhit ' 4. Feel free to set FlexiSpooler on workstations for IgnoreCertificateChainError = false'
Write-HostGray '    https://docs.ysoft.cloud/safeq6/latest/safeq6/setting-server-spooler-authentication-for-job-tran'
Write-HostWhit ' 5. Secure Distributed Layer Communication between SPOC Group members manually if you wish to have that link secured too'
Write-HostGray '    https://docs.ysoft.cloud/safeq6/latest/safeq6/how-to-secure-distributed-layer-communication'

$null = Read-Host  " press enter to exit the script"