PSAppDeployToolkit.Tools.psm1
<#
.SYNOPSIS PSAppDeployToolkit.Tools - companion module for PSAppDeployToolkit. .DESCRIPTION This module script contains functions to aid enterprise application packaging and the creation of PSAppDeployToolkit deployment scripts. PSAppDeployToolkit is licensed under the GNU LGPLv3 License - (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. .LINK https://psappdeploytoolkit.com #> #----------------------------------------------------------------------------- # # MARK: Module Initialization Code # #----------------------------------------------------------------------------- # Define modules needed to build out CommandTable. $RequiredModules = [System.Collections.ObjectModel.ReadOnlyCollection[Microsoft.PowerShell.Commands.ModuleSpecification]]$( @{ ModuleName = 'Microsoft.PowerShell.Management'; Guid = 'eefcb906-b326-4e99-9f54-8b4bb6ef3c6d'; ModuleVersion = '1.0' } @{ ModuleName = 'Microsoft.PowerShell.Utility'; Guid = '1da87e53-152b-403e-98dc-74d7b4d63d59'; ModuleVersion = '1.0' } @{ ModuleName = 'PSAppDeployToolkit'; Guid = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '3.93.0' } @{ ModuleName = 'PSScriptAnalyzer'; Guid = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0' } ) # Build out lookup table for all cmdlets used within module, starting with the core cmdlets. $CommandTable = [ordered]@{}; $ExecutionContext.SessionState.InvokeCommand.GetCmdlets() | & { process { if ($_.PSSnapIn -and $_.PSSnapIn.Name.Equals('Microsoft.PowerShell.Core') -and $_.PSSnapIn.IsDefault) { $CommandTable.Add($_.Name, $_) } } } (& $CommandTable.'Import-Module' -FullyQualifiedName $RequiredModules -Global -Force -PassThru -ErrorAction Stop).ExportedCommands.Values | & { process { $CommandTable.Add($_.Name, $_) } } # Set required variables to ensure module functionality. & $CommandTable.'New-Variable' -Name ErrorActionPreference -Value ([System.Management.Automation.ActionPreference]::Stop) -Option Constant -Force & $CommandTable.'New-Variable' -Name InformationPreference -Value ([System.Management.Automation.ActionPreference]::Continue) -Option Constant -Force & $CommandTable.'New-Variable' -Name ProgressPreference -Value ([System.Management.Automation.ActionPreference]::SilentlyContinue) -Option Constant -Force # Ensure module operates under the strictest of conditions. & $CommandTable.'Set-StrictMode' -Version 3 # Import this module's manifest via the language parser. This allows us to test with potential extra variables that are permitted in manifests. # https://github.com/PowerShell/PowerShell/blob/7ca7aae1d13d19e38c7c26260758f474cb9bef7f/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs#L509-L512 $Module = [System.Management.Automation.Language.Parser]::ParseFile("$PSScriptRoot\PSAppDeployToolkit.Tools.psd1", [ref]$null, [ref]$null).GetScriptBlock() $Module.CheckRestrictedLanguage([System.String[]]$null, [System.String[]]('PSEdition'), $true); $Module = & $Module # Store build information pertaining to this module's state. & $CommandTable.'New-Variable' -Name Module -Option Constant -Force -Value ([ordered]@{ Manifest = $Module Compiled = $MyInvocation.MyCommand.Name.Equals('PSAppDeployToolkit.Tools.psm1') }).AsReadOnly() # Remove any previous functions that may have been defined. if ($Module.Compiled) { & $CommandTable.'New-Variable' -Name FunctionNames -Option Constant -Value ($MyInvocation.MyCommand.ScriptBlock.Ast.EndBlock.Statements | & { process { if ($_ -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $_.Name } } }) & $CommandTable.'New-Variable' -Name FunctionPaths -Option Constant -Value ($FunctionNames -replace '^', 'Microsoft.PowerShell.Core\Function::') & $CommandTable.'Remove-Item' -LiteralPath $FunctionPaths -Force -ErrorAction Ignore } #----------------------------------------------------------------------------- # # MARK: Convert-ADTDeployment # #----------------------------------------------------------------------------- function Convert-ADTDeployment { <# .SYNOPSIS Converts either a Deploy-Application.ps1 script, or a full application package to use the new folder structure and syntax required by PSAppDeployToolkit v4. .DESCRIPTION The variables and main code blocks are updated to the new syntax via PSScriptAnalyzer, then transferred to a fresh Invoke-AppDeployToolkit.ps1 script. .PARAMETER Path Path to the Deploy-Application.ps1 file or folder to analyze. If a folder is specified, it must contain the Deploy-Application.ps1 script and the AppDeployToolkit folder. .PARAMETER Destination Path to the output file or folder. If not specified it will default to creating either a Invoke-AppDeployToolkit.ps1 file or FolderName_Converted folder under the parent folder of the supplied Path value. .PARAMETER Show Opens the newly created output in Windows Explorer. .PARAMETER Force Overwrite the output path if it already exists. .PARAMETER PassThru Pass the output file or folder to the pipeline. .INPUTS System.String You can pipe script files or folders to this function. .OUTPUTS System.IO.FileInfo / System.IO.DirectoryInfo Returns the file or folder to the pipeline if -PassThru is specified. .EXAMPLE Convert-ADTDeployment -Path .\Deploy-Application.ps1 This example converts Deploy-Application.ps1 into Invoke-AppDeployToolkit.ps1 in the same folder. .EXAMPLE Convert-ADTDeployment -Path .\PackageFolder This example converts PackageFolder into PackageFolder_Converted in the same folder. .EXAMPLE $ConvertedPackages = Get-ChildItem -Directory | Convert-ADTDeployment -Destination C:\Temp\ConvertedPackages -Force -PassThru Get all folders in the current directory and convert them to v4 packages in C:\Temp\ConvertedPackages, overwriting existing files and storing the new package info in $ConvertedPackages. .NOTES An active ADT session is NOT required to use this function. Requires PSScriptAnalyzer module 1.23.0 or later. To install: Install-Module -Name PSScriptAnalyzer -Scope CurrentUser Install-Module -Name PSScriptAnalyzer -Scope AllUsers Tags: psadt Website: https://psappdeploytoolkit.com Copyright: (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). License: https://opensource.org/license/lgpl-3-0 .LINK https://psappdeploytoolkit.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [ValidateScript({ if (!(& $Script:CommandTable.'Test-Path' -LiteralPath $_)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'The specified path does not exist.')) } elseif ((& $Script:CommandTable.'Test-Path' -LiteralPath $_ -PathType Leaf) -and [System.IO.Path]::GetExtension($_) -ne '.ps1') { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'The specified file is not a PowerShell script.')) } elseif ((& $Script:CommandTable.'Test-Path' -LiteralPath $_ -PathType Container) -and -not (& $Script:CommandTable.'Test-Path' -LiteralPath (& $Script:CommandTable.'Join-Path' -Path $_ -ChildPath 'Deploy-Application.ps1') -PathType Leaf)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'Deploy-Application.ps1 not found in the specified path.')) } elseif ((& $Script:CommandTable.'Test-Path' -LiteralPath $_ -PathType Container) -and -not (& $Script:CommandTable.'Test-Path' -LiteralPath (& $Script:CommandTable.'Join-Path' -Path $_ -ChildPath 'AppDeployToolkit') -PathType Container)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'AppDeployToolkit folder not found in the specified path.')) } return ![System.String]::IsNullOrWhiteSpace($_) })] [System.String]$Path, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Destination = (& $Script:CommandTable.'Split-Path' -Path $Path -Parent), [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$Show, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$Force, [Parameter(Mandatory = $false)] [System.Management.Automation.SwitchParameter]$PassThru ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $scriptReplacements = @( @{ v4FunctionName = 'Install-ADTDeployment' v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ine ''Uninstall''' }, @{ v4FunctionName = 'Uninstall-ADTDeployment' v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ieq ''Uninstall''' }, @{ v4FunctionName = 'Repair-ADTDeployment' v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ieq ''Repair''' } ) $variableReplacements = @('appVendor', 'appName', 'appVersion', 'appArch', 'appLang', 'appRevision', 'appScriptVersion', 'appScriptAuthor', 'installName', 'installTitle') $customRulePath = [System.IO.Path]::Combine($MyInvocation.MyCommand.Module.ModuleBase, 'PSScriptAnalyzer\Measure-ADTCompatibility.psm1') } process { try { try { $Path = (& $Script:CommandTable.'Resolve-Path' -LiteralPath $Path).Path $Destination = (& $Script:CommandTable.'Resolve-Path' -LiteralPath $Destination).Path $tempFolderName = "Convert-ADTDeployment_$([System.IO.Path]::GetRandomFileName().Replace('.', ''))" $tempFolderPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFolderName) if ($Path -like '*.ps1') { & $Script:CommandTable.'Write-Verbose' -Message "Input path is a .ps1 file: [$Path]" # Update destination path with a specific filename if current value does not end in .ps1 $Destination = if ($Destination -like '*.ps1') { $Destination } else { [System.IO.Path]::Combine($Destination, 'Invoke-AppDeployToolkit.ps1') } & $Script:CommandTable.'Write-Verbose' -Message "Destination path: [$Destination]" # Halt if the destination file already exists and -Force is not specified if (!$Force -and [System.IO.File]::Exists($Destination)) { $naerParams = @{ Exception = [System.IO.IOException]::new("File [$Destination] already exists.") Category = [System.Management.Automation.ErrorCategory]::ResourceExists ErrorId = 'FileAlreadyExistsError' TargetObject = $Path RecommendedAction = 'Use the -Force parameter to overwrite the existing file.' } & $Script:CommandTable.'Write-Error' -ErrorRecord (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } if ($Path -notmatch '(?<=^|\\)(Deploy-Application\.ps1|Invoke-AppDeployToolkit\.ps1)$') { & $Script:CommandTable.'Write-Warning' -Message "This function is designed to convert PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1." } # Create the temp folder & $Script:CommandTable.'Write-Verbose' -Message "Creating ADT Template in [$tempFolderPath]" & $Script:CommandTable.'New-ADTTemplate' -Destination ([System.IO.Path]::GetTempPath()) -Name $tempFolderName # Create a temp copy of the script to run ScriptAnalyzer fixes on - prefix filename with _ if it's named Invoke-AppDeployToolkit.ps1 $inputScriptPath = if ($Path -match '(?<=^|\\)Invoke-AppDeployToolkit.ps1$') { [System.IO.Path]::Combine($tempFolderPath, "_$([System.IO.Path]::GetFileName($Path))") } else { [System.IO.Path]::Combine($tempFolderPath, [System.IO.Path]::GetFileName($Path)) } & $Script:CommandTable.'Write-Verbose' -Message "Creating copy of [$Path] as [$inputScriptPath]" & $Script:CommandTable.'Copy-Item' -LiteralPath $Path -Destination $inputScriptPath -Force } else { & $Script:CommandTable.'Write-Verbose' -Message "Input path is a folder: [$Path]" # Re-use the same folder name with _Converted suffix for the new folder $folderName = "$(& $Script:CommandTable.'Split-Path' -Path $Path -Leaf)_Converted" # Update destination path to append this new folder name. If Destination is empty, it would mean that Path was something like C:\ with no parent, so just append the folder name to Path instead. $Destination = if ([string]::IsNullOrEmpty($Destination)) { [System.IO.Path]::Combine($Path, $folderName) } else { [System.IO.Path]::Combine($Destination, $folderName) } & $Script:CommandTable.'Write-Verbose' -Message "Destination path: [$Destination]" # Halt if the destination folder already exists and is not empty and -Force is not specified if (!$Force -and [System.IO.Directory]::Exists($Destination) -and ([System.IO.Directory]::GetFiles($Destination) -or [System.IO.Directory]::GetDirectories($Destination))) { $naerParams = @{ Exception = [System.IO.IOException]::new("Folder [$finalDestination] already exists and is not empty.") Category = [System.Management.Automation.ErrorCategory]::ResourceExists ErrorId = 'FolderAlreadyExistsError' TargetObject = $Path RecommendedAction = 'Use the -Force parameter to overwrite the existing folder.' } & $Script:CommandTable.'Write-Error' -ErrorRecord (& $Script:CommandTable.'New-ADTErrorRecord' @naerParams) } & $Script:CommandTable.'Write-Verbose' -Message "Creating ADT Template in [$tempFolderPath]" & $Script:CommandTable.'New-ADTTemplate' -Destination ([System.IO.Path]::GetTempPath()) -Name $tempFolderName & $Script:CommandTable.'Write-Verbose' -Message "Creating copy of [$Path\Deploy-Application.ps1] as [$tempFolderPath\Deploy-Application.ps1]" $inputScriptPath = (& $Script:CommandTable.'Copy-Item' -LiteralPath ([System.IO.Path]::Combine($Path, 'Deploy-Application.ps1')) -Destination $tempFolderPath -Force -PassThru).FullName } # Set the path of our v4 template script $outputScriptPath = [System.IO.Path]::Combine($tempFolderPath, 'Invoke-AppDeployToolkit.ps1') # First run the fixes on the input script to update function names and variables & $Script:CommandTable.'Write-Verbose' -Message "Running ScriptAnalyzer fixes on [$inputScriptPath]" & $Script:CommandTable.'Invoke-ScriptAnalyzer' -Path $inputScriptPath -CustomRulePath $customRulePath -Fix | & $Script:CommandTable.'Out-Null' # Parse the input script and find the if statement that contains the deployment code $inputScriptContent = & $Script:CommandTable.'Get-Content' -Path $inputScriptPath -Raw $inputScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($inputScriptContent, [ref]$null, [ref]$null) $ifStatement = $inputScriptAst.Find({ param ($ast) $ast -is [System.Management.Automation.Language.IfStatementAst] -and $ast.Clauses[0].Item1.Extent.Text -match $scriptReplacements[0].v3IfConditionMatch }, $true) if (-not $ifStatement) { throw "The expected if statement was not found in the input script." } foreach ($scriptReplacement in $scriptReplacements) { # Find the if clause to process from the v3 deployment script $ifClause = $ifStatement.Clauses | & $Script:CommandTable.'Where-Object' { $_.Item1.Extent.Text -match $scriptReplacement.v3IfConditionMatch } if ($ifClause) { & $Script:CommandTable.'Write-Verbose' -Message "Found statement: if ($($ifClause.Item1.Extent.Text))" # Re-read and parse the v4 template script after each replacement so that the offsets will still be valid $tempScriptContent = & $Script:CommandTable.'Get-Content' -Path $outputScriptPath -Raw $tempScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($tempScriptContent, [ref]$null, [ref]$null) # Find the function definition in the v4 template script to fill in $functionAst = $tempScriptAst.Find({ param ($ast) $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $ast.Name -eq $scriptReplacement.v4FunctionName }, $true) if ($functionAst) { & $Script:CommandTable.'Write-Verbose' -Message "Updating function [$($scriptReplacement.v4FunctionName)]" # Update the content of the v4 template script $start = $functionAst.Body.Extent.StartOffset $end = $functionAst.Body.Extent.EndOffset $scriptContent = $tempScriptAst.Extent.Text $newScriptContent = ($scriptContent.Substring(0, $start) + $ifClause.Item2.Extent.Text + $scriptContent.Substring($end)).Trim() # Fix to make converted InstallPhase declarations from v3 match the syntax used in the v4 template $newScriptContent = $newScriptContent -replace '\[String\]\$adtSession.InstallPhase = ''(Pre-|Post-)(?:Installation|Uninstallation|Repair)''', '$adtSession.InstallPhase = "$1$($adtSession.DeploymentType)"' -replace '\[String\]\$adtSession.InstallPhase = ''(?:Installation|Uninstallation|Repair)''', '$adtSession.InstallPhase = $adtSession.DeploymentType' #Fix to convert MARK labels to match v4 format $newScriptContent = $newScriptContent.Replace('##* MARK: PRE-INSTALLATION', '## MARK: Pre-Install').Replace('##* MARK: INSTALLATION', '## MARK: Install').Replace('##* MARK: POST-INSTALLATION', '## MARK: Post-Install').Replace('##* MARK: PRE-UNINSTALLATION', '## MARK: Pre-Uninstall').Replace('##* MARK: UNINSTALLATION', '## MARK: Uninstall').Replace('##* MARK: POST-UNINSTALLATION', '## MARK: Post-Uninstall').Replace('##* MARK: PRE-REPAIR', '## MARK: Pre-Repair').Replace('##* MARK: REPAIR', '## MARK: Repair').Replace('##* MARK: POST-REPAIR', '## MARK: Post-Repair') & $Script:CommandTable.'Set-Content' -Path $outputScriptPath -Value $newScriptContent -Encoding UTF8 } } else { & $Script:CommandTable.'Write-Warning' -Message "The if statement for [$($scriptReplacement.v4FunctionName)] was not found in the input script." } } # Re-read and parse the script one more time $tempScriptContent = & $Script:CommandTable.'Get-Content' -Path $outputScriptPath -Raw $tempScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($tempScriptContent, [ref]$null, [ref]$null) # Find the hashtable in the v4 template script that holds the adtSession splat $hashtableAst = $tempScriptAst.Find({ param ($ast) $ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $ast.Left.VariablePath.UserPath -eq 'adtSession' }, $true) if ($hashtableAst) { & $Script:CommandTable.'Write-Verbose' -Message 'Processing $adtSession hashtable' # Get the text form of the hashtable definition $hashtableContent = $hashtableAst.Right.Extent.Text # Copy each variable value from the input script to the hashtable foreach ($variableReplacement in $variableReplacements) { $assignmentAst = $inputScriptAst.Find({ param ($ast) $ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $ast.Left.Extent.Text -match "^(\[[^\]]+\])?\`$(adtSession\.)?$variableReplacement$" }, $true) if ($assignmentAst) { & $Script:CommandTable.'Write-Verbose' -Message "Updating variable [$variableReplacement]" $variableValue = $assignmentAst.Right.Extent.Text $hashtableContent = $hashtableContent -replace "(?m)(^\s*$variableReplacement\s*=)\s*'[^']*'", "`$1 $variableValue" } } & $Script:CommandTable.'Write-Verbose' -Message 'Updating variable [appScriptDate]' $hashtableContent = $hashtableContent -replace "(?m)(^\s*appScriptDate\s*=)\s*'[^']+'", "`$1 '$(& $Script:CommandTable.'Get-Date' -Format "yyyy-MM-dd")'" # Update the content of the v4 template script $start = $hashtableAst.Right.Extent.StartOffset $end = $hashtableAst.Right.Extent.EndOffset $scriptContent = $tempScriptAst.Extent.Text $newScriptContent = ($scriptContent.Substring(0, $start) + $hashtableContent + $scriptContent.Substring($end)).Trim() & $Script:CommandTable.'Set-Content' -Path $outputScriptPath -Value $newScriptContent -Encoding UTF8 } else { & $Script:CommandTable.'Write-Warning' -Message 'Could not find [$adtSession] hashtable' } & $Script:CommandTable.'Write-Verbose' -Message "Removing temp script [$inputScriptPath]" & $Script:CommandTable.'Remove-Item' -LiteralPath $inputScriptPath -Force if ($Path -like '*.ps1') { & $Script:CommandTable.'Write-Verbose' -Message "Moving file [$outputScriptPath] to [$Destination]" & $Script:CommandTable.'Move-Item' -LiteralPath $outputScriptPath -Destination $Destination -Force -PassThru:$PassThru # Display the newly created file in Windows Explorer (/select highlights the file in the folder). if ($Show) { & $Script:CommandTable.'Write-Verbose' -Message "Selecting [$Destination] in Windows Explorer" & ([System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows), 'explorer.exe')) /select, $Destination } } else { # If processing a folder, also copy the Files and SupportFiles subfolders foreach ($subFolder in 'Files', 'SupportFiles') { $subFolderPath = [System.IO.Path]::Combine($Path, $subFolder) if ([System.IO.Directory]::Exists($subFolderPath)) { & $Script:CommandTable.'Write-Verbose' -Message "Copying $subFolder content" & $Script:CommandTable.'Copy-Item' -LiteralPath $subFolderPath -Destination $tempFolderPath -Recurse -Force } } # Remove the Destination if it already exists (we should have already exited by this point if folder exists and Force not specified) if (& $Script:CommandTable.'Test-Path' -LiteralPath $Destination) { & $Script:CommandTable.'Write-Verbose' -Message "Removing existing destination folder [$Destination]" & $Script:CommandTable.'Remove-Item' -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue -WhatIf } # Sometimes previous actions were leaving a lock on the temp folder, so set up a retry loop for ($i = 0; $i -lt 5; $i++) { try { & $Script:CommandTable.'Write-Verbose' -Message "Moving folder [$tempFolderPath] to [$Destination]" & $Script:CommandTable.'Move-Item' -Path $tempFolderPath -Destination $Destination -Force -PassThru:$PassThru & $Script:CommandTable.'Write-Information' -MessageData "Conversion successful: $Destination" break } catch { & $Script:CommandTable.'Write-Verbose' -Message "Failed to move folder. Trying again in 500ms." [System.Threading.Thread]::Sleep(500) if ($i -eq 4) { throw } } } # Display the newly created folder in Windows Explorer. if ($Show) { & $Script:CommandTable.'Write-Verbose' -Message "Opening [$Destination] in Windows Explorer" & ([System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows), 'explorer.exe')) $Destination } } } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } finally { if (& $Script:CommandTable.'Test-Path' -LiteralPath $tempFolderPath) { & $Script:CommandTable.'Write-Verbose' -Message "Removing temp folder [$tempFolderPath]" & $Script:CommandTable.'Remove-Item' -Path $tempFolderPath -Recurse -Force -ErrorAction SilentlyContinue } } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Test-ADTCompatibility # #----------------------------------------------------------------------------- function Test-ADTCompatibility { <# .SYNOPSIS Tests a PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1 for any deprecated v3.x command or variable usage. .DESCRIPTION The Test-ADTCompatibility function run custom PSScriptAnalyzer rules against the input file and output any detected issues. The results can be output in a variety of formats. .PARAMETER FilePath Path to the .ps1 file to analyze. .PARAMETER Format Specifies the output format. The acceptable values for this parameter are: Raw, Table, Grid. The default value is Raw, which outputs the raw DiagnosticRecord objects from PSScriptAnalyzer. Table outputs just the line numbers and messages as a table. Grid outputs the line numbers and messages in a graphical window. .INPUTS System.String You can pipe script files to this function. .OUTPUTS Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord Returns the standard output from Invoke-ScriptAnalyzer. .EXAMPLE Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 This example analyzes Deploy-Application.ps1 and outputs the results. .EXAMPLE Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Table This example analyzes Deploy-Application.ps1 and outputs the results as a table. .EXAMPLE Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Grid This example analyzes Deploy-Application.ps1 and outputs the results as a grid view. .NOTES An active ADT session is NOT required to use this function. Requires PSScriptAnalyzer module 1.23.0 or later. To install: Install-Module -Name PSScriptAnalyzer -Scope CurrentUser Install-Module -Name PSScriptAnalyzer -Scope AllUsers Tags: psadt Website: https://psappdeploytoolkit.com Copyright: (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). License: https://opensource.org/license/lgpl-3-0 .LINK https://psappdeploytoolkit.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [ValidateScript({ if (!(& $Script:CommandTable.'Test-Path' -LiteralPath $_ -PathType Leaf)) { $PSCmdlet.ThrowTerminatingError((& $Script:CommandTable.'New-ADTValidateScriptErrorRecord' -ParameterName FilePath -ProvidedValue $_ -ExceptionMessage 'The specified file does not exist.')) } return ![System.String]::IsNullOrWhiteSpace($_) })] [System.String]$FilePath, [Parameter(Mandatory = $false)] [ValidateSet('Raw', 'Table', 'Grid')] [System.String]$Format = 'Raw' ) begin { # Initialize function. & $Script:CommandTable.'Initialize-ADTFunction' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $customRulePath = [System.IO.Path]::Combine($MyInvocation.MyCommand.Module.ModuleBase, 'PSScriptAnalyzer\Measure-ADTCompatibility.psm1') } process { try { try { $FilePath = (& $Script:CommandTable.'Resolve-Path' -LiteralPath $FilePath).Path if ($FilePath -notmatch '(Deploy-Application\.ps1|Invoke-AppDeployToolkit\.ps1)$') { & $Script:CommandTable.'Write-Warning' -Message "This function is designed to test PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1." } $results = & $Script:CommandTable.'Invoke-ScriptAnalyzer' -Path $FilePath -CustomRulePath $customRulePath switch ($Format) { 'Table' { $results | & $Script:CommandTable.'Format-Table' -AutoSize -Wrap -Property Line, Message } 'Grid' { $results | & $Script:CommandTable.'Select-Object' Line, Message | & $Script:CommandTable.'Out-GridView' -Title "Test-ADTCompatibility: $FilePath" -OutputMode None } 'Raw' { $results } } } catch { # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. & $Script:CommandTable.'Write-Error' -ErrorRecord $_ } } catch { # Process the caught error, log it and throw depending on the specified ErrorAction. & $Script:CommandTable.'Invoke-ADTFunctionErrorHandler' -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ } } end { # Finalize function. & $Script:CommandTable.'Complete-ADTFunction' -Cmdlet $PSCmdlet } } #----------------------------------------------------------------------------- # # MARK: Module Constants and Function Exports # #----------------------------------------------------------------------------- # Set all functions as read-only, export all public definitions and finalise the CommandTable. & $CommandTable.'Set-Item' -LiteralPath $FunctionPaths -Options ReadOnly & $CommandTable.'Get-Item' -LiteralPath $FunctionPaths | & { process { $CommandTable.Add($_.Name, $_) } } & $CommandTable.'New-Variable' -Name CommandTable -Value $CommandTable.AsReadOnly() -Option Constant -Force -Confirm:$false & $CommandTable.'Export-ModuleMember' -Function $Module.Manifest.FunctionsToExport # SIG # Begin signature block # MIIuKwYJKoZIhvcNAQcCoIIuHDCCLhgCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBObcevygnSzJIX # E1nx1at/DfR4ZD4sKIzkeGNcqrAaRKCCE5UwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdJMIIFMaADAgECAhAK+Vu2vqIMhQ6YxvuOrAj5MA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjQwOTA1MDAwMDAwWhcNMjcwOTA3MjM1OTU5WjCB0TET # MBEGCysGAQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhDb2xvcmFkbzEd # MBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xFDASBgNVBAUTCzIwMTMxNjM4 # MzI3MQswCQYDVQQGEwJVUzERMA8GA1UECBMIQ29sb3JhZG8xFDASBgNVBAcTC0Nh # c3RsZSBSb2NrMRkwFwYDVQQKExBQYXRjaCBNeSBQQywgTExDMRkwFwYDVQQDExBQ # YXRjaCBNeSBQQywgTExDMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA # uydxko2Hrl6sANJUjfdypKP60qBH5EkhfaRQAnn+e3vg2eVcbiEWIjlrMYzvK2sg # OMBbwGebqAURkFmUCKDdGxcxKeuXdaXPHWPKwc2WjYCFajrX6HofiiwNzOCdL6VE # 4PDQhPRR7SIdNNFSrx5C4ZDN1T6OH+ydX7EQF8+NBUNHRbEVdl+h9H5Aexx63afa # 8zu3g/GXluyXKbb+JHtgNJaUgFuFORTxw1TO6qH+S6Hrppf9QcAFmu4xGtkc2FSh # gv0NgWMNGDZqJr/o9sqJ2tdaZHDyr6H8PvY8egoUshF7ccgEYtEEdB9SRR8mVQik # 1w5oGTjDWjHj+8jgTpzletRywptk/m8PehVBN8ntqoSdvLLcuQVzmuPLzN/iuKh5 # sZeWvqPONApcEnZcONpXebyiUPnEePr5rZAU7hMjMw2ZPnQlMcbGvtgP2qi7m2f3 # mXFYxWjlKCxaApYHeqSFeWC8zM7OYL2HlZ+GuK4XG8jKVE6sWSW9Wk/dm0vJbasv # AgMBAAGjggICMIIB/jAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfROQjAd # BgNVHQ4EFgQU5GCU3SEqeIbhhY9eyU0LcTI75X8wPQYDVR0gBDYwNDAyBgVngQwB # AzApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYD # VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMIG1BgNVHR8Ega0wgaow # U6BRoE+GTWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH # NENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMFOgUaBPhk1odHRw # Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu # Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDCBlAYIKwYBBQUHAQEEgYcwgYQwJAYI # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQ # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29k # ZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkq # hkiG9w0BAQsFAAOCAgEAqA0ub/ilMgdIvMiBeWBoiMxe5OIblObGI7lemcP2WEqa # EASW11/wVwJU63ZwhtkQaNU4rXjf6fqy5pOUzpQXgYjSaO4D/AOMJKHlypxslFqZ # /dYpcue2xE3H7lmO4KPf8VxXuFIUqjLetU+kkh7o/Q52RabVAuOrPFKnObixy1HI # x0/5F+RuP9xhqmDbfM7l5zUAcuOCCkY7buuInEsip9BZXUiVb8K5bPR9Rk7Doat4 # FQmN72xjakcEZOMU/vg0ZgVa8nxkBXtVsjxbsr+bODn0cddHK1QHWil/PmpANkxN # 7H8tdCAZ8bTzIvvudxSLnt7ssbbQDkAyNw0btDH+MKv/l+VcYyQH51Z5xT9DvHCm # Ed774boZkP2GfTFvn7/gISEjTdOuUGstdrgSwg1zJPqgK7zWxK48xC7awpa3gwOs # 9pnyiqHG3rx84/SHUiAL2lkljsD3epmRxsWeZhZNY93xEpQHe9LBvo/t4VRjZzqU # z+pfEMPqeX/g5+mpb4ap6ZmNJuAYJFmU0LIkCLQN9mKXi1Il9WU6ifn3vYutGMSL # /BdeWP+7fM7MZLiO+1BIsBdSmV6pZVS3LRBAy3wIlbWL69mvyLCPIQ7z4dtfuzwC # 36E9k2vhzeiDQ+k1dFJDSdxTDetsck0FuD1ovhiu2caL4BdFsCWsXPLMyvu6OlYx # ghnsMIIZ6AIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg # UlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAr5W7a+ogyFDpjG+46sCPkwDQYJYIZI # AWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0B # CQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAv # BgkqhkiG9w0BCQQxIgQguJL5q03A4+uKkHlwS3FcsdknlyemgspQnZP0kfn6YqUw # DQYJKoZIhvcNAQEBBQAEggGASv/YA0GRo7qiJP803OuAsW0VSsh4BNUj7X6ZjDow # KziSncqOUA2RpVGhXKLIgwbeuwAm0Wco2Z0ZOcEpMVsMzT8vjZbzEk7sVpbWaQq6 # eoWgvTeYq0K+zNLOYhnJaTjKlB0ZmrY31/az+zJKJgOd6ePEYrG+kcym5Y9k5JnA # P1QXNQoJH512CjqfjWKHI5SsiqMROM6KL8+AR7615vZqzCMvGd19nsZPbZpoAalT # z/Dh0LY+/9UEdUT7G7TmqTswNP2VWNsRPEr+a271M+A7UQGRYp1FxBtzytXH0YEv # qvVkY8drWSJifAj3jlUvhzEhzrWPiHUDR4zzyQnN2C+mxQGyhx83lJuLlMzIoUW8 # JSetYPD2j5tvNBe0HxH1ClZK0aPyoX6kAX3gwWkPBbxRtni4J0yfp6vr2ek6O7em # YzUROlImG4xRMnq96yaYfmvIzARXqVjkE7KMl1OIak6gZwIvUwSRuFJjqyqS5q29 # U1FHmu8bpZn4NEa+9Q6wjbWQoYIXOTCCFzUGCisGAQQBgjcDAwExghclMIIXIQYJ # KoZIhvcNAQcCoIIXEjCCFw4CAQMxDzANBglghkgBZQMEAgEFADB3BgsqhkiG9w0B # CRABBKBoBGYwZAIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIP5PvtMU # v6meylPS87P3V/BO5QNmoiqZAdGP6kXnPjwZAhB7CySW8MoDcyxNK+ZGoUDSGA8y # MDI1MDEyNDE3NTQ0NVqgghMDMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnpBOMz # BDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNl # cnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBT # SEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEyNTIz # NTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYDVQQD # ExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIP # ADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBDEr4I # xHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo76EO # 7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rOH3bp # LEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9ReNZ8 # hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgXj3o5 # WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTVDSup # WJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16Jidj5 # XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/CacBq # U0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93NRxvd # 1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1XCB+1 # rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMBAAGj # ggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8E # DDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # HwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9XLAN3 # DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3Rh # bXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRw # Oi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l # U3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQSR9lD # kfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWBb0Hv # qT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDCzFzU # y34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1UruJ # KlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3WpByXt # gVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGEsshJ # mLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8a1u7 # cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNFYagL # DBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7QEY7M # hKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgEdeoH # NHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/JceEN # c2Sg8h3KeFUCS7tpFk7CrDqkMIIGrjCCBJagAwIBAgIQBzY3tyRUfNhHrP0oZipe # WzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl # cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdp # Q2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIwMzIzMDAwMDAwWhcNMzcwMzIyMjM1 # OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5 # BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0 # YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxoY1Bkmz # wT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCwzIP5WvYRoUQVQl+kiPNo+n3znIkL # f50fng8zH1ATCyZzlm34V6gCff1DtITaEfFzsbPuK4CEiiIY3+vaPcQXf6sZKz5C # 3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ7Gnf2ZCHRgB720RBidx8ald68Dd5 # n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7QKxfst5Kfc71ORJn7w6lY2zkpsUd # zTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/tePc5OsLDnipUjW8LAxE6lXKZYnLvWH # po9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCYOjgRs/b2nuY7W+yB3iIU2YIqx5K/ # oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9KoRxrOMUp88qqlnNCaJ+2RrOdOqPV # A+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6dSgkQe1CvwWcZklSUPRR8zZJTYsg # 0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM1+mYSlg+0wOI/rOP015LdhJRk8mM # DDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbCdLI/Hgl27KtdRnXiYKNYCQEoAA6E # VO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbECAwEAAaOCAV0wggFZMBIGA1UdEwEB # /wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1NhS9zKXaaL3WMaiCPnshvMB8GA1Ud # IwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNV # HSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0 # dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2Vy # dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0f # BDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB # MA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7ZvmKlEIgF+ZtbYIULhsBguEE0TzzBT # zr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI2AvlXFvXbYf6hCAlNDFnzbYSlm/E # UExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/tydBTX/6tPiix6q4XNQ1/tYLaqT5Fm # niye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVPulr3qRCyXen/KFSJ8NWKcXZl2szw # cqMj+sAngkSumScbqyQeJsG33irr9p6xeZmBo1aGqwpFyd/EjaDnmPv7pp1yr8TH # wcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc6UsCUqc3fpNTrDsdCEkPlM05et3/ # JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3cHXg65J6t5TRxktcma+Q4c6umAU+9 # Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0dKNPH+ejxmF/7K9h+8kaddSweJywm # 228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZPJ/tgZxahZrrdVcA6KYawmKAr7ZVB # tzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLeMt8EifAAzV3C+dAjfwAL5HYCJtnw # ZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDyDivl1vupL0QVSucTDh3bNzgaoSv2 # 7dZ8/DCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEM # BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ # RCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIIC # IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zC # pyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf # 1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x # 4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEio # ZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7ax # xLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZ # OjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJ # l2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz # 2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH # 4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb # 5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ # 9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYD # VR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuC # MS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYI # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3 # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5j # b20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0g # ADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs # 7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq # 3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/ # Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9 # /HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWoj # ayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3YwggNyAgEB # MHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYD # VQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFt # cGluZyBDQQIQC65mvFq6f5WHxvnpBOMzBDANBglghkgBZQMEAgEFAKCB0TAaBgkq # hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MDEyNDE3 # NTQ0NVowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU29OF7mLb0j575PZxSFCHJNWG # W0UwLwYJKoZIhvcNAQkEMSIEIFOcgxHWMTnLqouDZ8bISF7MrtQMT7rwxmUCx7fV # G7uCMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIHZ2n6jyYy8fQws6IzCu1lZ1/tdz # 2wXWZbkFk5hDj5rbMA0GCSqGSIb3DQEBAQUABIICACW6Ui8XXEQmbG0hGWtxE1kZ # 01XfdVV+i6YPIUBwn0aolNMBkyc+PHNjUVM+TO+EpSXFifef9wGbYLGsCBSEmuNw # R4YA+RGMrietNtDE2hxOzK7UMR3PFHDhpUioXk7G7YVBiQx4/71Sj2Dny734lr/x # Kb1YXFpvKI7z6U+nKE2e6Ig5wapoi8iZAM/3zMAbp9exoP01IEJS8gVFyqbm6o1N # 0/Nz/nPHD5y4vc4que5MDKzYE44OmIzS1TFUE/2dvwnZ9SFAmGl77WT16ZwYQ+Ib # M3+JS0NzJGgNDlhYVMbsMe9y8DILp175L8d5GDB0s/kvGeRcPq7vxubMPskF7xcY # RtxSFeG4ikEglrt9yqbrK189jaQ/MWPa4CjNRuCbn7feBaasrJM6T7IXUnHGdmKb # fFMoNsM9veE4a8mGhYRPCgH+4lOByQKUG82ueFh9KCqPpSoCqcnjOnTRW3K+JF/e # 0iGqUPzbmbB501sOaK5Gg/kmktgA4RmkzpSdufuEaLCVLizH30dTra4F07ljKp4s # eeRK8ifD7Eevws3ABPA7oyG2Dyj50xrGMK9mI8vSoQ02YyEYMiSD0iPIZls+qVTC # LHDQ5bfgISFTedDfEpS7EocnlHmL75AFOGMZj76FbQW7K2I3BNl9WE64132GMKd2 # +qK6IaSg9zLPfeT17UjX # SIG # End signature block |