TD.Util.psm1
<# .SYNOPSIS Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store .DESCRIPTION Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local. .PARAMETER Url Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany) .Example $token = Get-AzureDevOpsAccessToken 'https://mycompany@dev.azure.com/mycompany') #> function Get-AzureDevOpsAccessToken([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url) { $token = $env:SYSTEM_ACCESSTOKEN if ([string]::IsNullOrEmpty($token)) { if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force } Import-Module CredentialManager $credential = Get-StoredCredential -Target "git:$Url" if ($null -eq $credential) { Throw "No Azure DevOps credentials found in credential store" } Write-Verbose "Using Azure DevOps Access Token from Windows Credential Store" $token = $credential.GetNetworkCredential().Password } return $token } <# .SYNOPSIS Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store .DESCRIPTION Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local. .PARAMETER Url Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany) .Example $cred = Get-AzureDevOpsCredential 'https://mycompany@dev.azure.com/mycompany') #> function Get-AzureDevOpsCredential([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url) { $token = $env:SYSTEM_ACCESSTOKEN if ([string]::IsNullOrEmpty($token)) { if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force } Import-Module CredentialManager $credential = Get-StoredCredential -Target "git:$Url" if ($null -eq $credential) { Throw "No Azure DevOps credentials found. It should be passed in via env:SYSTEM_ACCESSTOKEN." } Write-Verbose "Using Azure DevOps Access Token from Windows Credential Store" } else { Write-Verbose "Using Azure DevOps Access Token from Hosted Agent" $secureToken = $token | ConvertTo-SecureString -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential(".", $secureToken) } return $credential } <# .SYNOPSIS Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts .DESCRIPTION Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts .PARAMETER PackageSource Azure DevOps packagesource name .PARAMETER Modules Array of modules to import .PARAMETER Credential Credentials to access feed .PARAMETER Latest Always import latest modules .EXAMPLE Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2 Import-AzureDevOpsModules -PackageSource 'myFeed' -Modules @('myModule') -Latest #> function Import-AzureDevOpsModules([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackageSource, [Parameter(Mandatory = $true)]$Modules, [System.Management.Automation.PSCredential]$Credential, [Switch]$Latest) { foreach ($module in $Modules) { if (-not (Get-Module -ListAvailable -Name $module) -or $Latest.IsPresent) { Install-Module $module -Repository $PackageSource -Scope CurrentUser -Force -AllowClobber -Credential $Credential } else { Import-Module $module } } } <# .SYNOPSIS Publish the PowerShell Package to the Azure Devops Feed / Artifacts .DESCRIPTION Publish the PowerShell Package to the Azure Devops Feed / Artifacts. Depends on nuget.exe installed and in environment path. Strategy: - Register feed with nuget - Register local temp feed to use Powershell Publish-Module command - Publish locally created module to feed with nuget.exe .PARAMETER ModuleName Name of the PowerShell Module to publish .PARAMETER ModulePath Root path of the module .PARAMETER Feedname Name of the Azure DevOps feed .PARAMETER FeedUrl Url of the Azure DevOps feed .PARAMETER AccessToken Personal AccessToken used for Azure DevOps Feed push/publish .Example Publish-PackageToAzureDevOps -ModuleName 'MyModule' -ModulePath './Output' -Feedname 'MyFeed' -FeedUrl 'https://pkgs.dev.azure.com/mycompany/_packaging/MyFeed/nuget/v2' -AccessToken 'sasasasa' #> function Publish-PackageToAzureDevOps([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$ModuleName, $ModulePath = './Output', [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Feedname, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$FeedUrl, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$AccessToken) { $packageSource = $Feedname $packageFeedUrl = $FeedUrl $deployPath = Join-Path $ModulePath $ModuleName # register nuget feed $nuGet = (Get-Command 'nuget').Source &$nuGet sources Remove -Name $packageSource [string]$r = &$nuGet sources if (!($r.Contains($packageSource))) { # add as NuGet feed Write-Verbose "Add NuGet source" &$nuGet sources Add -Name $packageSource -Source $packageFeedUrl -username "." -password $AccessToken } # get module version $manifestFile = "./$ModuleName/$ModuleName.psd1" $manifest = Import-PowerShellDataFile -Path $manifestFile $version = $manifest.Item('ModuleVersion') if (!$version) { Throw "No module version found in $manifestFile" } else { Write-Host "$moduleName version: $version" } $tmpFeedPath = Join-Path ([System.IO.Path]::GetTempPath()) "$(New-Guid)-Localfeed" New-Item -Path $tmpFeedPath -ItemType Directory -ErrorAction Ignore -Force | Out-Null try { # register temp feed for export package if (Get-PSRepository -Name LocalFeed -ErrorAction Ignore) { Unregister-PSRepository -Name LocalFeed } Register-PSRepository -Name LocalFeed -SourceLocation $tmpFeedPath -PublishLocation $tmpFeedPath -InstallationPolicy Trusted # publish to temp feed $packageName = "$moduleName.$version.nupkg" $package = (Join-Path $tmpFeedPath $packageName) Write-Verbose "Publish Module $package" Publish-Module -Path $deployPath -Repository LocalFeed -Force -ErrorAction Ignore if (!(Test-Path $package)) { Throw "Nuget package $package not created" } # publish package from tmp/local feed to PS feed Write-Verbose "Push package $packageName in $tmpFeedPath" Push-Location $tmpFeedPath try { nuget push $packageName -source $packageSource -Apikey Az -NonInteractive if ($LastExitCode -ne 0) { Throw "Error pushing nuget package $packageName to feed $packageSource ($packageFeedUrl)" } } finally { Pop-Location } } finally { Remove-Item -Path $tmpFeedPath -Force -Recurse } } <# .SYNOPSIS Registers a package source from AzureDevOps Feed / Artifacts .DESCRIPTION Registers a package source from AzureDevOps Feed /Artifacts. If already found removes reference first. .PARAMETER Name Name of package source .PARAMETER Url Url of package feed .PARAMETER Credential Credentials to access feed .Example Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2 #> function Register-AzureDevOpsPackageSource([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Name, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url, [System.Management.Automation.PSCredential]$Credential) { if ($Credential) { try { Invoke-RestMethod -Uri $Url -Credential $Credential | Out-Null # check for access to artifacts with credential } catch { Throw "Register-AzureDevOpsPackageSource error for $Url : $($_.Exception.Message)" } } if (Get-PSRepository -Name $Name -ErrorAction Ignore) { Unregister-PSRepository -Name $Name } Register-PSRepository -Name $Name -SourceLocation $Url -InstallationPolicy Trusted -Credential $Credential } <# .SYNOPSIS Get Azure Keyvault secrets and add them to token collection .DESCRIPTION Get secrets from Azure Keyvault and add them to token collection, use default logged-in account to Azure or try to get it from 'az cli' .PARAMETER Vault Name of the Azure KeyVault .PARAMETER Tokens Hashtable to add secrets to .PARAMETER SubscriptionId Azure Subscription ID .Example $Tokens = @{} Add-TokensFromAzureKeyVault -Vault 'MyVaultName' -Tokens $Tokens -SubscriptionId 'mySubscriptionId' #> function Add-TokensFromAzureKeyVault([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Vault, [Parameter(Mandatory = $true)]$Tokens, $SubscriptionId) { Write-Verbose "Add-TokensFromAzureKeyVault" Write-Verbose " Vault: $Vault" Write-Verbose " SubscriptionId: $SubscriptionId" function Add-Secret($Name, $Value) { if (!$Tokens.ContainsKey($Name)) { Write-Host "Adding secret $Name : ******* to Token Store" $Tokens.Add($Name, $Value) } } Connect-ToAzure if ($SubscriptionId) { Select-AzureDefaultSubscription -SubscriptionId $SubscriptionId } $warning = (Get-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings -ErrorAction Ignore) -eq 'true' Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true" try { $secrets = Get-AzKeyVaultSecret -VaultName $Vault foreach ($secret in $secrets) { try { $s = Get-AzKeyVaultSecret -VaultName $Vault -Name $secret.Name } Catch [Microsoft.Azure.KeyVault.Models.KeyVaultErrorException] { # ignore disabled/expired secrets } #$pass = $s.SecretValue | ConvertFrom-SecureString -AsPlainText $cred = New-Object System.Management.Automation.PSCredential($secret.Name, $s.SecretValue) Add-Secret $secret.Name $cred } } finally { Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings $warning } } <# .SYNOPSIS Assert if logged-in to Azure with powershell Az modules .DESCRIPTION Assert if logged-in to Azure with powershell Az modules .Example Assert-AzureConnected #> function Assert-AzureConnected { Initialize-AzureModules $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile if (-not $azProfile.Accounts.Count) { Throw "Powershell Az error: Ensure you are logged in." } } <# .SYNOPSIS Connect to Azure with Powershell Az modules .DESCRIPTION Connect to Azure with Powershell Az modules, use 'az cli' as fallback to connect .PARAMETER Force Always re-authenticated when used .Example Connect-ToAzure #> function Connect-ToAzure([Switch]$Force) { Write-Verbose "Connect-ToAzure" # check already logged-in to Azure if (!(Test-AzureConnected) -or $Force.IsPresent) { # try to find logged-in user via az cli if installed Write-Verbose 'Connect to azure with Azure Cli configuration' try { $token = $(az account get-access-token --query accessToken --output tsv) $id = $(az account show --query user.name --output tsv) if ($token -and $id) { Connect-AzAccount -AccessToken $token -AccountId $id -Scope Process } } catch { # use default, already connected user in this session } } Assert-AzureConnected $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile Write-Verbose "Az Account: $($azProfile.DefaultContext.Account.Id)" Write-Verbose "Az Subscription: $($azProfile.DefaultContext.Subscription.Name) - $($azProfile.DefaultContext.Subscription.Id)" } <# .SYNOPSIS Initializes (install or import) the Azure Az modules into current Powershell session .DESCRIPTION Initializes (install or import) the Azure Az modules into current Powershell session .Example Initialize-AzureModules #> function Initialize-AzureModules { if ($Global:AzureInitialized) { return } if ($null -eq (Get-Module -ListAvailable 'Az')) { Install-Module -Name Az -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az)) { Import-Module Az -Scope local -Force } } if ($null -eq (Get-Module -ListAvailable 'Az.Accounts')) { Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az.Accounts)) { Import-Module Az.Accounts -Scope local -Force } } if ($null -eq (Get-Module -ListAvailable 'Az.KeyVault')) { Install-Module -Name Az.KeyVault -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az.KeyVault)) { Import-Module Az.KeyVault -Scope local -Force } } $Global:AzureInitialized = $true } $Global:AzureInitialized = $false <# .SYNOPSIS Select the Azure default subscription .DESCRIPTION Select the Azure default subscription .PARAMETER SubscriptionId The Azure subscription Id .Example Select-AzureDefaultSubscription -SubscriptionId 'myid' #> function Select-AzureDefaultSubscription([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$SubscriptionId) { Assert-AzureConnected $ctxList = Get-AzContext -ListAvailable foreach ($ctx in $ctxList) { if ($ctx.Subscription.Id -eq $SubscriptionId) { Write-Verbose "Select context: $($ctx.Name)" Select-AzContext -Name $ctx.Name return } } Throw "Azure subscription '$SubscriptionId' not found" } <# .SYNOPSIS Test if logged-in to Azure with powershell Az modules .DESCRIPTION Test if logged-in to Azure with powershell Az modules .Example Test-AzureConnected #> function Test-AzureConnected { Initialize-AzureModules try { $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile return !(-not $azProfile.Accounts.Count) } catch { return $false } } <# .SYNOPSIS Get tokens from config repository and add them to token collection .DESCRIPTION Get tokens from xml config repository and add them to token collection .PARAMETER ConfigPath Root path of the xml config files .PARAMETER Tokens Hashtable to add tokens to .PARAMETER Env Token environment filter, filter the tokens by environent like local, develop, test etc... .Example $Tokens = @{} Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local' #> function Add-TokensFromConfig([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$ConfigPath, [Parameter(Mandatory = $true)]$Tokens, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Env, $Module) { function Add-Var($Nodes, $NameProp = 'name', $ValueProp = 'value', $Prefix) { foreach ($node in $Nodes) { $name = $node."$NameProp" $value = $node."$ValueProp" if ($value -and $value.StartsWith('$')) { $value = Invoke-Expression "Write-Output `"$($value)`"" } $pre = $Prefix if ($node.LocalName -eq 'node') { if ($node.ParentNode.ParentNode.name -ne $Env) { continue } } elseif ($node.LocalName -eq 'system-user') { if ($node.ParentNode.LocalName -eq 'application') { $pre = "$Prefix$($node.ParentNode.name)" } } if ($pre) { $kn = "$pre$name" Write-Host "Adding variable $kn : $value to Token Store" if (!$Tokens.ContainsKey($kn)) { $Tokens.Add($kn, $value) } } else { if (!$Tokens.ContainsKey($name)) { Write-Host "Adding variable $name : $value to Token Store" $Tokens.Add($name, $value) } } } } function Add-Modules($Nodes, $Module) { foreach ($node in $Nodes) { if ($Module -and ($node.name -ne $Module)) { continue } Write-Host "Adding module $($node.name) to Token Store" $Tokens.Add("module-$($node.name)", $node.name) $Tokens.Add("module-$($node.name)-role", $node.role) $Tokens.Add("module-$($node.name)-depends", $node.depends) $Tokens.Add("module-$($node.name)-folder", $node.folder) $nodeApps = $node.SelectNodes(".//application") foreach ($nodeApp in $NodeApps) { Write-Host "Adding module $($node.name) application $($nodeApp.name) to Token Store" $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)", $nodeApp.name) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-type", $nodeApp.type) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-role", $nodeApp.role) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-service", $nodeApp.service) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-exe", $nodeApp.exe) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-dotnet-version", $nodeApp.'dotnet-version') } } } $modules = @() Get-ChildItem "$ConfigPath\*.xml" -Recurse | ForEach-Object { $doc = [xml] (Get-Content $_.Fullname) $nodes = $doc.SelectNodes("//variable[@environment='$Env' or not(@environment)]") Add-Var $nodes $nodes = $doc.SelectNodes("//node") if ($nodes.Count -gt 0) { Add-Var $nodes -NameProp 'role' -ValueProp 'name' -Prefix 'node-' } $nodes = $doc.SelectNodes("//service[@environment='$Env' or not(@environment)]") if ($nodes.Count -gt 0) { Add-Var $nodes -Prefix 'service-' Add-Var $nodes -Prefix 'service-cert-hash-' -ValueProp 'cert-hash' } $nodes = $doc.SelectNodes("//system-user[@environment='$Env' or not(@environment)]") if ($nodes.Count -gt 0) { Add-Var $nodes -NameProp 'system-user' -ValueProp 'name' -Prefix 'system-user-' } $nodes = $doc.SelectNodes("//module") if ($nodes.Count -gt 0) { Add-Modules -Nodes $nodes -Module $Module $nodes | ForEach-Object { $modules += $_.name } } $envNode = $doc.SelectSingleNode("//environment[@name='$Env']") if ($envNode) { $Tokens.Add('env-name', $envNode.'name') $Tokens.Add('env-group', $envNode.'group') $Tokens.Add('env-name-short', $envNode.'name-short') $Tokens.Add('env-name-suffix', $envNode.'name-suffix') $Tokens.Add('env-type', $envNode.'type') $Tokens.Add('env-active', $envNode.'active') $Tokens.Add('env-domain', $envNode.'domain') $Tokens.Add('env-domain-full', $envNode.'domain-full') $Tokens.Add('env-domain-description', $envNode.'description') $Tokens.Add('env-domain-owner', $envNode.'owner') $Tokens.Add('env-domain-notes', $envNode.'notes') $Tokens.Add('env-ps-remote-user', $envNode.'ps-remote-user') $Tokens.Add('env-subscription-id', $envNode.'subscription-id') $Tokens.Add('env-vault', $envNode.'vault') } } $Tokens.Add('modules', $modules) } <# .SYNOPSIS Convert the tokens in file to their actual values .DESCRIPTION Convert the tokens in file to their actual values .PARAMETER FileName Name of the file to convert .PARAMETER PrefixToken Token prefix .PARAMETER SuffixToken Token suffix .PARAMETER DestFileName File name of converted file .PARAMETER ShowTokensUsed Switch to echo tokens replaced .PARAMETER SecondPass Switch to signal that same file is used in multiple conversions .PARAMETER Tokens Hashtable to add tokens to .Example $Tokens = @{} Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local' Get-ChildItem .\$ConfigLocation\*.* | ForEach-Object { $destFile = Join-Path $ArtifactsLocation $_.Name Convert-TokensInFile -FileName $_.Fullname -DestFileName $destFile -Tokens $Tokens } #> function Convert-TokensInFile([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$FileName, $PrefixToken = '__', $SuffixToken = '__', $DestFileName, [Switch]$ShowTokensUsed, [Switch]$SecondPass, $Tokens) { if (!$DestFileName) { $DestFileName = $FileName } if (Test-Path $FileName) { $regex = [regex] "${PrefixToken}((?:(?!${SuffixToken}).)*)${SuffixToken}" $content = [System.IO.File]::ReadAllText($FileName); if (!$Tokens) { $Tokens = @{} } $script:cnt = 0 $callback = { param([System.Text.RegularExpressions.Match] $Match) $value = $Match.Groups[1].Value # check env first $newTokenValue = [Environment]::GetEnvironmentVariable($value) if ($null -eq $newTokenValue) { if ($Tokens.ContainsKey($value)) { $v = $Tokens[$value] if ($v -is [PSCredential]) { $newTokenValue = $v.GetNetworkCredential().Password } else { $newTokenValue = $v.ToString() # detect expression in variable if ($newTokenValue.StartsWith('$')) { $newTokenValue = Invoke-Expression "Write-Output `"$($newTokenValue)`"" } } } } if ($null -eq $newTokenValue) { $script:HasReplaceVarErrors = $true; Write-Warning "Token not found in replace: '$value'" return "" } $script:cnt++ if ($ShowTokensUsed.IsPresent -or ($Global:VerbosePreference -eq 'Continue')) { Write-Host "Replacing token '$value' with '$newTokenValue'" } return $newTokenValue } $content = $regex.Replace($content, $callback) New-Item -ItemType Directory (Split-Path -Path $DestFileName) -Force -ErrorAction Ignore | Out-Null Set-Content -Path $DestFileName -Value $content -Encoding UTF8 if ($Global:VerbosePreference -eq 'Continue') { if ($SecondPass.IsPresent -and ($script:cnt -eq 0) ) { Write-Host "Tokens replaced: $($script:cnt)" } else { Write-Host "Tokens replaced: $($script:cnt)" } } } else { Throw "Convert-TokensInFile error file not found '$FileName'" } } <# .SYNOPSIS Send a msg to Slack Channel .DESCRIPTION Send a msg to Slack Channel via the Incoming Webhook integration App. See in slack: Browse Apps / Custom Integrations / Incoming WebHook or see notes below .PARAMETER Msg The message to send .PARAMETER Channel The Channel to send to .PARAMETER Username The user of the message .PARAMETER IconUrl The url of the icon to display in the message, otherwise use emoji .PARAMETER Emoji The emoji to use like ':ghost:' or ':bom:' see slack documentation for more Emoji. Use IconUrl for custom emoji .PARAMETER AsUser Send msg as this User .PARAMETER Token The Incoming WebHook Token .PARAMETER Attachements The json structured attachement. See Slack documentation like $attachment = @{ fallback = $msg pretext = "Sample message: <http://url_to_task|Test out Slack message attachments>" color = "danger" # good, warning fields = @( @{ title = "[Alert]]" value = "This is much easier than I thought it would be. <https://www.sample.com/logo.png>|Logo" short = "false" } ) } .Example Send-ToSlack -m 'Hello' -c 'TestChannel' -u 'me' -e ':bomb:' -t 'mytoken...' .NOTES for documentation about configuring Slack/Aquire token see https://api.slack.com/messaging/webhooks or https://api.slack.com/legacy/custom-integrations #> function Send-ToSlack ([alias('m')]$Msg, [alias('c')]$Channel, [alias('u')]$Username, [alias('iu')]$IconUrl, [alias('e')]$Emoji, [alias('a')][Switch]$AsUser, [alias('t')]$Token, $Attachments) { $slackUri = "https://hooks.slack.com/services/$Token" if ($Channel -and !($Channel.StartsWith('@'))) { $channel = "#$Channel" } else { $channel = $Channel } $body = @{ channel = $channel username = $Username text = $Msg icon_url = $IconUrl icon_emoji = $Emoji } if ($null -eq $Emoji) { $body.Remove('icon_emoji') } if ($null -eq $IconUrl ) { $body.Remove('icon_url') } if ($Attachments) { [void]$body.Add('attachments', $Attachments) } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $response = Invoke-RestMethod -Uri $slackUri -Method Post -Body ($body | ConvertTo-Json -Compress -Depth 10) -ContentType 'application/json' } catch { Throw "Send-ToSlack error: $($_.Exception.Message)" } if ($response -ne 'ok') { Throw "Send-ToSlack error: $($response)" } } # SIG # Begin signature block # MIIf8gYJKoZIhvcNAQcCoIIf4zCCH98CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUY23R49XMqz4atNVpQGRgRpkJ # DPCgghr1MIIE/jCCA+agAwIBAgIQDUJK4L46iP9gQCHOFADw3TANBgkqhkiG9w0B # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz # c3VyZWQgSUQgVGltZXN0YW1waW5nIENBMB4XDTIxMDEwMTAwMDAwMFoXDTMxMDEw # NjAwMDAwMFowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMTCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBAMLmYYRnxYr1DQikRcpja1HXOhFCvQp1dU2UtAxQ # tSYQ/h3Ib5FrDJbnGlxI70Tlv5thzRWRYlq4/2cLnGP9NmqB+in43Stwhd4CGPN4 # bbx9+cdtCT2+anaH6Yq9+IRdHnbJ5MZ2djpT0dHTWjaPxqPhLxs6t2HWc+xObTOK # fF1FLUuxUOZBOjdWhtyTI433UCXoZObd048vV7WHIOsOjizVI9r0TXhG4wODMSlK # XAwxikqMiMX3MFr5FK8VX2xDSQn9JiNT9o1j6BqrW7EdMMKbaYK02/xWVLwfoYer # vnpbCiAvSwnJlaeNsvrWY4tOpXIc7p96AXP4Gdb+DUmEvQECAwEAAaOCAbgwggG0 # MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG # AQUFBwMIMEEGA1UdIAQ6MDgwNgYJYIZIAYb9bAcBMCkwJwYIKwYBBQUHAgEWG2h0 # dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAfBgNVHSMEGDAWgBT0tuEgHf4prtLk # YaWyoiWyyBc1bjAdBgNVHQ4EFgQUNkSGjqS6sGa+vCgtHUQ23eNqerwwcQYDVR0f # BGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl # ZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz # c3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6 # Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NB # LmNydDANBgkqhkiG9w0BAQsFAAOCAQEASBzctemaI7znGucgDo5nRv1CclF0CiNH # o6uS0iXEcFm+FKDlJ4GlTRQVGQd58NEEw4bZO73+RAJmTe1ppA/2uHDPYuj1UUp4 # eTZ6J7fz51Kfk6ftQ55757TdQSKJ+4eiRgNO/PT+t2R3Y18jUmmDgvoaU+2QzI2h # F3MN9PNlOXBL85zWenvaDLw9MtAby/Vh/HUIAHa8gQ74wOFcz8QRcucbZEnYIpp1 # FUL1LTI4gdr0YKK6tFL7XOBhJCVPst/JKahzQ1HavWPWH1ub9y4bTxMd90oNcX6X # t/Q/hOvB46NJofrOp79Wz7pZdmGJX36ntI5nePk2mOHLKNpbh6aKLzCCBTEwggQZ # oAMCAQICEAqhJdbWMht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X # DTE2MDEwNzEyMDAwMFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx # MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBD # QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnF # OVQoV7YjSsQOB0UzURB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQA # OPcuHjvuzKb2Mln+X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhis # EeTwmQNtO4V8CdPuXciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQj # MF287DxgaqwvB8z98OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+f # MRTWrdXyZMt7HgXQhBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW # /5MCAwEAAaOCAc4wggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAf # BgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/ # AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEF # BQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBD # BggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDBQBgNVHSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYc # aHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZI # hvcNAQELBQADggEBAHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafD # DiBCLK938ysfDCFaKrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6 # HHssIeLWWywUNUMEaLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4 # H9YLFKWA1xJHcLN11ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHK # eZR+WfyMD+NvtQEmtmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIo # xhhWz0E0tmZdtnR79VYzIi8iNrJLokqV2PWmjlIwggVUMIIEPKADAgECAhADvQgp # hDR84QsbE/VhFNISMA0GCSqGSIb3DQEBCwUAMH0xCzAJBgNVBAYTAkdCMRswGQYD # VQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNV # BAoTEUNPTU9ETyBDQSBMaW1pdGVkMSMwIQYDVQQDExpDT01PRE8gUlNBIENvZGUg # U2lnbmluZyBDQTAeFw0xODA4MDMwMDAwMDBaFw0yMTA4MDIyMzU5NTlaMIGiMQsw # CQYDVQQGEwJOTDEPMA0GA1UEEQwGMzExMlZNMQswCQYDVQQIDAJaSDERMA8GA1UE # BwwIU2NoaWVkYW0xJjAkBgNVBAkMHVByb2YgS2FtZXJsaW5naCBPbm5lc2xhYW4g # MjEyMRwwGgYDVQQKDBNUZWRvbiBUZWNobm9sb2d5IEJWMRwwGgYDVQQDDBNUZWRv # biBUZWNobm9sb2d5IEJWMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA # rZsUekPypcNCThOuoGGQTNbTwu0ALM6bNUtk8qg5cFy0J9BSLRn7YRFCXppDm28x # n6mlYFcVRJpxWtlww6VuJO9Tnc5dZRfiwWq01QDaQXEi3XULPcqO6HGtr2bEMRBV # hPabzDRpeqrB6dV63JFCK4xF6avQysK0V7iWU7zUBk7XpKGaHDHok6BJDEpet0o0 # IkNyCky+KdrVwGIs5sF/VjuMPAV0jWyR6Y4Ad/VN5H9kR1XULDnm8JT6I7RIiAhS # 0dhZpgMAhJ+gBvmwPDXjbTOsBEIRV7ZTnL/LpmxF2haJsOWlaeCvO5gBHAo1e/Dz # Ol01/fPfyK+L4BidT2CMUQIDAQABo4IBqDCCAaQwHwYDVR0jBBgwFoAUKZFg/4pN # +uv5pmq4z/nmS71JzhIwHQYDVR0OBBYEFGrbyp14YxvdJLazSUYrCph3JB9CMA4G # A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMD # MBEGCWCGSAGG+EIBAQQEAwIEEDBGBgNVHSAEPzA9MDsGDCsGAQQBsjEBAgEDAjAr # MCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8ubmV0L0NQUzBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9SU0FD # b2RlU2lnbmluZ0NBLmNybDB0BggrBgEFBQcBAQRoMGYwPgYIKwYBBQUHMAKGMmh0 # dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNvZGVTaWduaW5nQ0EuY3J0 # MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wGQYDVR0RBBIw # EIEOaW5mb0B0ZWRvbi5jb20wDQYJKoZIhvcNAQELBQADggEBAHzJAjy8aywbanqK # OKF2VO7kh0zdeyI24GR26n5BK1PzLHBOgCpA94UyZZsQfiSVqrHnq4hNN/x8duv5 # z9yTx+pll4o1LT1gyXHRmNaNw5v9/N8yGnd08lP1Cxkc1H34pJfXnU2U36bmQNeZ # 4L2IIfTau5ym/uZQ10VsI2lCnVdB+WrR6EIpP5bFqjlEV+6sQC+8WSFNGXyFGZaW # 2ICiA/ls+GbGPhJfzjSM1mh8h1vHQc2KHgAixqfJHUrk/FN2G/i+BG5bO5zytg3E # DvG7E2+ew6+UQZsF7VPyIdjwd/7e0oq42eEfg6VlBHysPickewcLfaMAQgeHM60H # Ntv/v/QwggV+MIIEZqADAgECAhBn3vQ+8Xva4k/1lAYG0sCEMA0GCSqGSIb3DQEB # DAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIx # EDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSEw # HwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2VydmljZXMwHhcNMDQwMTAxMDAwMDAw # WhcNMjgxMjMxMjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0 # ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RP # IENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB # dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpW # sawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh # 7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7E # pi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV/erBvGy2 # i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEfZd5ICLqk # Tqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX2nwz # V0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3p # N/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1 # DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpV # Rr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abfFobg2P9A # 3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiqcrxXStJL # nbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo4HyMIHvMB8GA1UdIwQYMBaA # FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBS7r34CPfqm8TyEjq3uOJjs # 2TIy1DAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI # MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j # b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM # BQADggEBAH/yVjWwbZVKTnSvOuJvAYuH0zKX7fhA0ndTEdfHFi7GneZIVr6Aqfi8 # eNLIYxeujO0WMfofGMkOx+5IeZ/Hybm8zIgV42hh0Z8dS2GB11YEY8IIaSbw8OUv # 38AKK6kF9AJaaonXtIRClePr93YgXjXZwM0lCBNMcTiOh7AzhJGZHpHxrJ4/px1g # gSw2QVSg4kYGC6wbx5k2jF6hC6Se2UJGJMXFW4GuraCg3J82uI3CHRX6iK2BEDkf # RPArn90QVAwHNLE20RT9BwI9/3JVqyfWLIFBcSmNQfRQVxp+ZWCvy8Uodpius6hT # dovmIVJr6iHQhA5JTohT2pIu5x0IZtcwggXgMIIDyKADAgECAhAufIfMDpNKUv6U # /Ry3zTSvMA0GCSqGSIb3DQEBDAUAMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMS # R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD # T01PRE8gQ0EgTGltaXRlZDErMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0 # aW9uIEF1dGhvcml0eTAeFw0xMzA1MDkwMDAwMDBaFw0yODA1MDgyMzU5NTlaMH0x # CzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNV # BAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSMwIQYDVQQD # ExpDT01PRE8gUlNBIENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQAD # ggEPADCCAQoCggEBAKaYkGN3kTR/itHd6WcxEevMHv0xHbO5Ylc/k7xb458eJDIR # J2u8UZGnz56eJbNfgagYDx0eIDAO+2F7hgmz4/2iaJ0cLJ2/cuPkdaDlNSOOyYru # Ggxkx9hCoXu1UgNLOrCOI0tLY+AilDd71XmQChQYUSzm/sES8Bw/YWEKjKLc9sMw # qs0oGHVIwXlaCM27jFWM99R2kDozRlBzmFz0hUprD4DdXta9/akvwCX1+XjXjV8Q # wkRVPJA8MUbLcK4HqQrjr8EBb5AaI+JfONvGCF1Hs4NB8C4ANxS5Eqp5klLNhw97 # 2GIppH4wvRu1jHK0SPLj6CH5XkxieYsCBp9/1QsCAwEAAaOCAVEwggFNMB8GA1Ud # IwQYMBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBQpkWD/ik366/mm # arjP+eZLvUnOEjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAT # BgNVHSUEDDAKBggrBgEFBQcDAzARBgNVHSAECjAIMAYGBFUdIAAwTAYDVR0fBEUw # QzBBoD+gPYY7aHR0cDovL2NybC5jb21vZG9jYS5jb20vQ09NT0RPUlNBQ2VydGlm # aWNhdGlvbkF1dGhvcml0eS5jcmwwcQYIKwYBBQUHAQEEZTBjMDsGCCsGAQUFBzAC # hi9odHRwOi8vY3J0LmNvbW9kb2NhLmNvbS9DT01PRE9SU0FBZGRUcnVzdENBLmNy # dDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3 # DQEBDAUAA4ICAQACPwI5w+74yjuJ3gxtTbHxTpJPr8I4LATMxWMRqwljr6ui1wI/ # zG8Zwz3WGgiU/yXYqYinKxAa4JuxByIaURw61OHpCb/mJHSvHnsWMW4j71RRLVIC # 4nUIBUzxt1HhUQDGh/Zs7hBEdldq8d9YayGqSdR8N069/7Z1VEAYNldnEc1PAuT+ # 89r8dRfb7Lf3ZQkjSR9DV4PqfiB3YchN8rtlTaj3hUUHr3ppJ2WQKUCL33s6UTmM # qB9wea1tQiCizwxsA4xMzXMHlOdajjoEuqKhfB/LYzoVp9QVG6dSRzKp9L9kR9Gq # H1NOMjBzwm+3eIKdXP9Gu2siHYgL+BuqNKb8jPXdf2WMjDFXMdA27Eehz8uLqO8c # GFjFBnfKS5tRr0wISnqP4qNS4o6OzCbkstjlOMKo7caBnDVrqVhhSgqXtEtCtlWd # vpnncG1Z+G0qDH8ZYF8MmohsMKxSCZAWG/8rndvQIMqJ6ih+Mo4Z33tIMx7XZfiu # yfiDFJN2fWTQjs6+NX3/cjFNn569HmwvqI8MBlD7jCezdsn05tfDNOKMhyGGYf6/ # VXThIXcDCmhsu+TJqebPWSXrfOxFDnlmaOgizbjvmIVNlhE8CYrQf7woKBP7aspU # jZJczcJlmAaezkhb1LU3k0ZBfAfdz/pD77pnYf99SeC7MH1cgOPmFjlLpzGCBGcw # ggRjAgEBMIGRMH0xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNo # ZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1p # dGVkMSMwIQYDVQQDExpDT01PRE8gUlNBIENvZGUgU2lnbmluZyBDQQIQA70IKYQ0 # fOELGxP1YRTSEjAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK # KwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUVetP6kplch+owqyZ7/16YKNVOkgw # DQYJKoZIhvcNAQEBBQAEggEAOkkq6RY05eLSrKoz8EQWBWCxmSwnWgBDHD04uGu8 # OUUurID5aQtNtTEVIRW/8z0vtkU4lmoxoM9AiiR1ZXWe+duXVpPTeop9t+O/DN/V # 2GDuCicBOR8/prO9HJOrWW4D2muQyO8rtopwww5YD1kvDOEdmlkqa/NYatjGCIvh # xAKIzfgtqkIFXPCX2qSjgXfmjUyUq4d2TJj75mAyzvamG3Ggba+1pOW2i9KdOLjn # Lkhoe0N+UBHS1qUzkxmy0t10D4xBKOQ08H9RFPMRoDQ+P+QOYhz4hjBigGzd+vhM # 4/w0RsxlLtB9VSln5m7C2KGAEqaVIPm+0ibYU1Fm+bgYcqGCAjAwggIsBgkqhkiG # 9w0BCQYxggIdMIICGQIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhE # aWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBAhANQkrgvjqI # /2BAIc4UAPDdMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3 # DQEHATAcBgkqhkiG9w0BCQUxDxcNMjEwMzI0MTc0MTM3WjAvBgkqhkiG9w0BCQQx # IgQgAG0xOvAOgBxOlufI6qbtB6g5uqgqiF+VfnNrqTykgpUwDQYJKoZIhvcNAQEB # BQAEggEAwN0DnLY/OIo3TfA84oF+1L2ll3RKtVvU/VSPZdNyNXKADwG1PUMgEoar # zOlGfU90ksMlVLCSeLGPL3CFjpPhcvVOF5/2R/zavXg9kPW+g8gGoMDTpH3ZqDvi # 504z+qC9LGXaa55x4ndiqjQNvjMLlRAjXNaagJVAnVyEGmKrWgRWQiK888xCf8W7 # MnziyWMEWLS9wyb+smsNh5glTtJdQsY6O8f9+cIaZSX1n05bW9+NjhS2BFQ72WkX # 9X2rDTHvsC+GBq2+c3KHA0zI+w3fPkyVdXCuy8pEZL43jzzOPSH1kfbDmdunj3CW # aBLQ2ZXsiuaImfyW5NA4dEmqKWvJXg== # SIG # End signature block |