AppManiProgramManager.psm1

# Version: 1.51.0 Date: 240309 Last udpate by:
# + Added function Add-UploadDataFile and Get-GUID
# Version: 1.50.2 Date: 240309 Last udpate by:
# ! Fixed 7-zip where it was giving the download link for beta version
# Version: 1.50.1 Date: 240304 Last udpate by:
# ! Fixed Philips Device Connector
# Version: 1.50.0 Date: 240222 Last udpate by:
# + Added Github Desktop
# Version: 1.49.0 Date: 240214 Last udpate by:
# + Added Agent Ransack
# Version: 1.48.2 Date: 240214 Last udpate by:
# * Added error handling for Get-InstalledProgram for instances where the registry keys do not exist
# Version: 1.48.1 Date: 240130 Last udpate by:
# + Fixed download and latest version links for Synology, Citrix Workspace, and, Actionstep Office Add-In, WinSCP Yealink USB Connect
# + Added ISQLME, LandOnline Print-to-tiff Driver, NZCS IT Support, UCS Client download links
# ! Improved Confirm-InstallerDigitalSignature
# + Added LEAP Desktop
# Version: 1.48.0 Date: 240118 Last udpate by:
# + Added function Get-DownloadLinkV2 where it returns objects containing download links of both x64 and x86 architectures of a program if available
# / Various changes to functions to accomodate new install and update script process
# ! Get-ProcessToTerminate output will no longer default to the program name if not defined
# Version: 1.47.0 Date: 240112 Last udpate by:
# + Added functions Get-InstallerMetaData and Confirm-InstallerHash
# / Changed function name Confirm-InstallerValidity to Confirm-InstallerDigitalSignature
# Version: 1.46.1 Date: 240105 Last update by: rod@appmani.com
# ! Fixed Dropbox download link where it still picks up an older version
# / Changed Write-Log to use Foreground color from Background color
# + Added post-install step to Sysmon64 to check Sysmon exe's file version.
# Version: 1.46.0 Date: 231222 Last update by: rod@appmani.com
# + Added feature to enable selection of sysmon config or refresh current its current configuration.
# + Added post-install step to Sysmon to verify if event logs are successfully created.
# Version: 1.45.2 Date: 231215 Last update by: rod@appmani.com
# + Fixed download link and latest version number code for Audacity, Citrix Workspace, Wireshark, Yealink USB Connect
# Version: 1.45.1 Date: 231215 Last update by: rod@appmani.com
# + Fixed LegalAid Templates download link and latest version number code
# Version: 1.45.0 Date: 231203 Last update by: rod@appmani.com
# + Converted various Write-Log as Debug type
# Version: 1.44.7 Date: 231122 Last update by: rod@appmani.com
# ! Set default value for JsonDepth in Set-Alert
# Version: 1.44.6 Date: 231118 Last update by: rod@appmani.com
# + Added debug mode in Write-Log
# / Converted some Write-Log calls as DEBUG types.
# Version: 1.44.5 Date: 231115 Last update by: rod@appmani.com
# + Added josn depth parameter for Set-Alert
# + Added parameter to set background color in Write-Log
# Version: 1.44.4 Date: 231028 Last update by: rod@appmani.com
# + Enabled architecture selection for Zoom
# Version: 1.44.3 Date: 231028 Last update by: rod@appmani.com
# ! Fixed Zoom bug by modifying version number scraped to omit the patch version as it is not included in the display version when installed
# Version: 1.44.2 Date: 231025 Last update by: rod@appmani.com
# ! Added pauses between retries on Get-Installer and Get-DownloadLink
# ! Corrected NitroPDF Pro download link
# ! Fixed bug in Wireshark where global architecture variable is not set to x64 in the scaneario where user is notified that x86 is no longer available.
# Version: 1.44.1 Date: 231020 Last update by: rod@appmani.com
# ! Fixed service name in Patriot Task Service's post-install script
# + Addded Bartender 2022 download link and latest version number scraping code
# Version: 1.44.0 Date: 231011 Last update by: rod@appmani.com
# + Added Arcserve ShadowProtect
# Version: 1.43.3 Date: 231009 Last update by: rod@appmani.com
# / Updated Arcserve ShadowControl post-install command to use altport
# Version: 1.43.2 Date: 231009 Last update by: rod@appmani.com
# + Added [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 to functions that does HTTP requests
# Version: 1.43.1 Date: 231009 Last update by: rod@appmani.com
# ! Fixed bug where installer is unable to download due to "The underlying connection was closed: An unexpected error occurred on a send"
# ! Fixed bug on TrendMicro post-install script where savePath was not initialized
# Version: 1.43.0 Date: 231007 Last update by: rod@appmani.com
# + Added warnings in pre/post-install scriptblocks when program specific variables are null or empty
# + Added a step to check if installation was successful or not before executing post-isntall scripts
# + Added post-install script for Arcserve Shadow Control
# ! Ensured log file is displayed during Trend Mirco post-install script
# Version: 1.42.0 Date: 231006 Last update by: rod@appmani.com
# + Added Ajax PRO Desktop
# Version: 1.41.0 Date: 231004 Last update by: rod@appmani.com
# + Added Arcserve ShadowControl
# Version: 1.40.1 Date: 230930 Last update by: rod@appmani.com
# / Removed pre-install script for TeamViewer Full to extract zip containing MSI as the file coming from Scripter will be the msi itself
# + Added TeamViewer Full in programs that need to be uninstalled first before reinstalling.
# Version: 1.40.0 Date: 230930 Last update by: rod@appmani.com
# + Added WinSCP
# + Added Function Get-RedirectedUrl to retrieve what a URL is being redirected to
# Version: 1.39.0 Date: 230927 Last update by: rod@appmani.com
# + Added Trend Micro Security Agent
# Version: 1.38.1 Date: 230922 Last update by: rod@appmani.com
# ! Added try catch in Get-LowerVersionConflicts to ignore errors when unable to compare versions due to parsing issues.
# Version: 1.38.0 Date: 230921 Last update by: rod@appmani.com
# + Added Patriot VLC Extension
# Version: 1.37.2 Date: 230906 Last update by: rod@appmani.com
# / Changed code to retrieve Digisign version number to just include the major and minor version.
# Version: 1.37.1 Date: 230901 Last update by: rod@appmani.com
# / Re-added Yealink USB Connect
# Version: 1.37.0 Date: 230831 Last update by: rod@appmani.com
# + Added Yealink USB Connect
# Version: 1.36.5 Date: 230831 Last update by: rod@appmani.com
# ! Removed a line used for debugging
# Version: 1.36.4 Date: 230831 Last update by: rod@appmani.com
# ! Fixed code for retrieving latest version number for Mozilla Firefox
# Version: 1.36.3 Date: 230826 Last update by: rod@appmani.com
# / Changed how Read-RegistryValueData retrieves registry values by returning an array of objects containing registry keys and values (including non-existent ones). Previous method only returns registry keys with existing values.
# Version: 1.36.2 Date: 230824 Last update by: rod@appmani.com
# / Programs without a method of retrieving the latest version number will proceed with update scripts by default.
# Version: 1.36.1 Date: 230823 Last update by: rod@appmani.com
# + Added WinRAR uninstall command override
# Version: 1.36.0 Date: 230823 Last update by: rod@appmani.com
# + Added WinRAR
# Version: 1.35.0 Date: 230822 Last update by: rod@appmani.com
# + Read-RegistryValueData now supports wildcards on registry key paths.
# Version: 1.34.0 Date: 230809 Last update by: rod@appmani.com
# + Added Sophos Endpoint Agent
# Version: 1.33.0 Date: 230809 Last update by: rod@appmani.com
# + Added Nitro PDF Pro
# ! Fixed Adobe Acrobat and Adobe Digital Editions download link and latest version code
# Version: 1.32.0 Date: 230731 Last update by: rod@appmani.com
# + Added Patriot Reporting Components, Task Service, and Version 6 Client
# Version: 1.31.0 Date: 230726 Last update by: rod@appmani.com
# + Added function Remove-RegistryKey
# Version: 1.30.0 Date: 230711 Last update by: rod@appmani.com
# + Added ServiceCATRMM, Actionstep Office Add-In, and Snagit
# Version: 1.29.0 Date: 230625 Last update by: rod@appmani.com
# + Added support for Microsoft Edge in the function Add-BrowserExtension
# + Added function Resolve-BrowserExtensionID to verify if an extention exists based on ExtensionID
# + Added function Get-BrowserExtensionID to return an extension ID based on extension name.
# Version: 1.28.0 Date: 230618 Last update by: rod@appmani.com
# / Remove-InstallerFolder sets location to C:\ProgramData\AppMani\ServiceCATRMM\work to ensure installer folder is not in use in the terminal at the time of deletion
# + Added Philips Device Connector
# + Added function Add-BrowserExtension (Does not support Edge yet as Edge and Chrome no longer share an extensions "store").
# Version: 1.27.0 Date: 230617 Last update by: rod@appmani.com
# ! Fixed IrfanView download link retrieval code
# / Added > $null to running install commands so it won't output anything in the console
# / Confirm-ProgramInstallation now accepts ProgramType as a parameter to make way for service installations scripts.
# + Added Advanced IP Scanner and Sysmon64
# + Added functions Approve-ProgramInstall, Approve-ProgramUpdate, Show-ProgramQuickInfo
# Version: 1.26.1 Date: 230531 Last update by: rod@appmani.com
# ! Fixed TreeSize Free silent uninstall code
# ! Fixed LegalAid Templates pre-install code
# ! Fixed TeamViewer and TeamViewer Host cases in pre-install scriptblock switch
# ! Fixed 7-zip and Google Chrome latest version number scraping code
# + Added log in Get-InstalledProgram when no program was found based on search criteria
# + Added Windirstat latest version number scraping code
# + Added UCS Client uninstall code
# Version: 1.26.0 Date: 230531 Last update by: rod@appmani.com
# + Added TeamViewer installation
# + Extracted code that determines filename from a download to its own function Get-InstallerFileName
# * Refactored some code for improved code readability
# Version: 1.25.4 Date: 230525 Last update by: rod@appmani.com
# + Parameterized Zello Server for ZelloWork
# Version: 1.25.3 Date: 230525 Last update by: rod@appmani.com
# ! Fixed bug on retrieving Digisign Uninstall Command
# * Improved error handling when deleting folder and files in Add-InstallerFolder
# Version: 1.25.2 Date: 230523 Last update by: rod@appmani.com
# ! Fixed bug where Get-Installer will return null on the first failed attempt and will not re-attempt.
# + Added download link for Digisign Repair Tool
# ! Fixed bug where pre-install scriptblock for Digisign gets executed for Digisign Repair Tool
# Version: 1.25.1 Date: 230523 Last update by: rod@appmani.com
# ! Get-InstalledProgram will now error if a null RegistryDisplayNameRegex is supplied.
# Version: 1.25.0 Date: 230521 Last update by: rod@appmani.com
# + Added breaks to each Get-DownloadLink case
# + Added DC Loader, Remote Access Tool, LandOnline Print-to-Tiff Driver
# + Added function Get-PostInstallScriptBlock
# Version: 1.24.0 Date: 230517 Last update by: rod@appmani.com
# + Added TeamViewer Host pre-install script block, install command
# ! Applied temporary fix on Confirm-ProgramInstallation to cater older version of install scripts
# Version: 1.23.0 Date: 230514 Last update by: rod@appmani.com
# + Added ISQLME, NZCS IT Support, WindirStat, and ZelloWork
# ! Fixed bug on Get-InstalledProgram where it throws an error when a program doesn't have a DisplayVersion property in the registry
# Version: 1.22.1 Date: 230512 Last update by: rod@appmani.com
# ! Applied overrides to GPL Ghostscript latest version/downloadlink and Java 8 download link
# Version: 1.22.0 Date: 230511 Last update by: rod@appmani.com
# + Added functions Find-Path, Get-ArchitectureConflicts, Get-LowerVersionConflicts and Uninstall-LowerVersionConflicts
# / Changed Invoke-ResolveArchitectureConflicts to Uninstall-ArchitectureConflicts
# / Uninstalling architecture conflicts was moved out of Approve-Installation function
# / Confirm-ProgramInstallation only now needs ProgramName.
# ! Fixed UniPrint Pre-install scriptblock
# Version: 1.21.0 Date: 230423 Last update by: rod@appmani.com
# + Added UCS Client installation code
# / Moved out Find-Path as a new function
# ! Fixed PreInsstallScriptBlock switch case for CutePDF Writer|Putty|Digisign|Google Drive
# Version: 1.20.1 Date: 230423 Last update by: rod@appmani.com
# ! Fixed code for scraping download links and latest version numbers for Adobe Digital Editions, Foxit PDF Reader, Java 8, Network Lookout for Employees Pro, Sysmon64, and Microsoft Teams
# Version: 1.20.0 Date: 230423 Last update by: rod@appmani.com
# + Added function Get-PreferredArchitecture and Resolve-ArchitectureSelection to cater new architecture selections Preferred and Use Existing. These functions are integrated to Get-Downloadlink.
# ! Fixed code for retrieving latest version and download link for Wireshark
# Version: 1.19.0 Date: 150423 Last update by: rod@appmani.com
# + Added option to select update channel during Microsft 365 installation
# Version: 1.19.0 Date: 150423 Last update by: rod@appmani.com
# + Added functions Get-M365SupportedVersion and Get-M365UpdateChannel
# Version: 1.18.7 Date: 020423 Last update by: rod@appmani.com
# ! Fixed formatting errors on install/uninstall commands
# Version: 1.18.6 Date: 020423 Last update by: rod@appmani.com
# ! Fixed Adobe Acrobat/Digital Editions script stuck bug when Get-LatestVersion is called the second time.
# Version: 1.18.5 Date: 310323 Last update by: rod@appmani.com
# / Added pre-install steps to Adobe Digital Edition to add some registry keys to skip Norton installation prompt.
# Version: 1.18.4 Date: 270323 Last update by: rod@appmani.com
# ! Fixed bug where the script terminates after running Disable-IEFirstRunCustomization
# + Added function Get-IgnoreExitCodes to cater installations where we have to make exceptions on certain exit codes e.g. Adobe Digital Edition
# + Added function Get-IgnoreExitCodes to adjust for installations that gives an unusual exit code
# + Added Adobe Digital Editions to supported applications
# Version: 1.18.3 Date: 240323 Last update by: rod@appmani.com
# ! Added a delay after uninstalling programs before continuing updates.
# Version: 1.18.2 Date: 220323 Last update by: rod@appmani.com
# ! Provided an override for Digisign's uninstall command
# Version: 1.18.1 Date: 210323 Last update by: rod@appmani.com
# ! Fixed install command for Zoom
# ! Fixed code for retrieving latest version number for Adobe Acrobat
# Version: 1.18.0 Date: 170323 Last update by: rod@appmani.com
# / Set the option to use strings.exe via a switch parameter
# + Added function Approve-Installation and Invoke-ResolveArchitectureConflicts to remediate duplicate installations on different/same architectures before installing. The logic for determining if a program is up for an update is moved here too.
# + Added function Get-ProcessesToTerminate
# Version: 1.17.3 Date: 250223 Last update by: rod@appmani.com
# ! Fixed bug where Get-UninstallCommand still tries to use strings.exe
# Version: 1.17.2 Date: 240223 Last update by: rod@appmani.com
# + Added Microsoft 365 functions
# Version: 1.17.1 Date: 190223 Last update by: rod@appmani.com
# + Added Synology Active Backup for Business Agent functions
# Version: 1.17.0 Date: 190223 Last update by: rod@appmani.com
# / Changed program name for Python to 'Python 3'
# + Added functions Get-InstallCommand and Get-PreInstallScriptBlock
# * Add-RegistryKey can now recursively create missing registry keys recursively
# / Add-RegistryValue can now only add single values from multiple values. Looping will be handled in the scripts.
# Version: 1.16.0 Date: 050223 Last update by: rod@appmani.com
# + Added registry management functions
# / Changed $Error references to $Global:Error
# Version: 1.15.0 Date: 290123 Last update by: rod@appmani.com
# / Replaced method of determining installer tool from strings.exe to PowerShell native commands
# Version: 1.14.0 Date: 280123 Last update by: rod@appmani.com
# * Improved program architecture detection function
# Version: 1.13.1 Date: 210123 Last update by: rod@appmani.com
# ! Fixed bug where SYSTEM is unable to run strings.exe
# Version: 1.13.0 Date: 210123 Last update by: rod@appmani.com
# + Added Synology Drive Client latest version and download link retrieval
# + Added step on Get-UninstallCommand where the function can try to determine silent uninstall switch based on what tool the uninstall executable was made from.
# Version: 1.12.3 Date: 110123 Last Updated by: rod@appmani.com
# ! Fixed Adobe Acrobat latest version retrieval bug
# Version: 1.12.2 Date: 080123 Last Update by: rod@appmani.com
# + Added download link and latest version retrieval code for LegalAid Templates
# ! Fixed bug where comparing versions errors when it's too short.
# Version: 1.12.0 Date: 080123 Last Update by: rod@appmani.com
# + Added Get-MultipleInstalledProgram function
# / Get-UninstallCommand now uses wildcards to find uninstall command overrides
# + Added Get-ProgramRegistryDisplayRegex
# + Added Approve-SelectedPrograms
# Version: 1.11.3 Date: 231222 Last Update by: rod@appmani.com
# ! Fixed Adobe Acrobat bug
# Version: 1.11.2 Date: 221222 Last Update by: rod@appmani.com
# ! Overriden the version on Adobe Acrobat while fixing bug that occurs when the code detects a version that has an 'x' on it.
# Version: 1.11.0 Date: 221221 Last Update by: rod@appmani.com
# + Added functions Get-UninstallCommand and UninstallProgram
# + Added new registry paths for Get-InstalledProgram
# / Get-InstalledProgram only returns the topmost result if more than 1 results are returned.
# ! Fixed IrfanView download link retrieval function
# ! Fixed message when download link cannot be retrieved.
# Version: 1.10.0 Date: 221202 Last Update by: rod@appmani.coms
# + Added function Set-Alert
# Version: 1.9.2 Date: 221122 Last Update by: rod@appmani.com
# * Improved error handling on Get-LatestVersion/DownloadLink/Installer
# Version: 1.9.1 Date: 221117 Last Update by: rod@appmani.com
# + Added security measures to Remove-InstallerFolder to prevent deleting C:\ uninstentionally
# * Improved error handling in Confirm-Update and Get-LatestVersion
# Version: 1.9.0 Date: 221113 Last Updated by: rod@appmani.com
# + Add function Get-LatestVersion, Confirm-LogFolder, Write-Log, Disable-IEFirstRunCustomization
# / Replaced usage of Write-Host functions to Write-Log
# * Downloading installers now able to supply download file name independently
# / Get-InstalledProgram now uses a regex so you can match specific programs
# / Confirm-Program/ServiceInstallation changed retry times to 3 from 30
# / Changed Compare-Versions to Confirm-Update where this function utilizes the Get-LatestVersion function to determine if program is due for an update
# Version: 1.8.0 Date: 221011 Last Updated by: rod@appmani.com
# + Added function Set-AgentRefresh
# Version: 1.7.0 Date: 221011 Last Updated by: rod@appmani.com
# + Added function Confirm-InstallerValidity
# Version: 1.6.0 Date: 221009 Last Updated by: rod@appmani.com
# + Added function Get-DownloadLink
# Version: 1.5.0 Date: 220916 Last Updated by: rod@appmani.com
# + Added function Invoke-ModuleForUpdate
# Version: 1.4.1 Date: 220817 Last Updated by: rod@appmani.com
# / Fixed Get-ProgramArchitecture's output from x32 to x86
# Version: 1.4.0 Date: 220817 Last Updated by: rod@appmani.com
# / Changed Get-InstalledProgram/Service parameter 'Program' to 'ProgramName'
# / Get-InstalledProgram's use of wildcards will only be used depending on function call's parameters
# + Added new functions Set-RegistryItem, Get-ProgramArchitecture
# * Improved error handling responses
# 1.3.2 ! Fixed a syntax error
# 1.3.1 - Removed some lines for debugging
# 1.3.0 * Set ProgressPreference to SilentlyContinue to improve download time
# + Now displays current and latest available version of program
# 1.2.2 + Added functions Add-InstallerFolder and Remove-InstallerFolder in functions to export in module manifest
# 1.2.1 - Removed uneeded files in package
# 1.2.0 + Added Add-InstallerFolder function so all installer related files will go to a single folder
# / Changed Remove-Installer function to Remove-InstallerFolder
# 1.1.2 / Changed version just to test updates
# 1.1.1 / Changed author to Appmani
# 1.1.0 + Added Confirm-ServiceInstallation function
# 1.0.0 + First upload

# This function is to mitigate the Invoke-WebRequest error where it won't run because IE First Run Customization hasn't been done yet. Using the switch parameter UseBasicParsing would work for regular web requests, but not for Downloads
Function Test-WebRequest {
    Param (
        $URI
    )

    # Loop until system is able to successfully invoke a web request
    while ($null -eq $webRequest) {
        try {
            $webRequest = Invoke-WebRequest -Uri $URI
        }
        # Catches the exception where IE first run customization has not been done yet
        catch [System.NotSupportedException] {
            Write-Host "Disabling IE First RunCustomization..." -NoNewline
            try {
                Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main" -Name "DisableFirstRunCustomize" -Value 2
            }
            catch {
                Write-Log -LogType ERROR -Message "Failed to disable IE First RunCustomization: $($Global:Error[0])"
                return $null
            }
        }
        # catches other exceptions
        catch {
            Write-Log -LogType ERROR -Message "Failed to execute test webrequest: $($Global:Error[0])"
            return $null
        }
    }
    return $webrequest
}

Function Get-InstallerFileName {
    Param (
        $Download
    )

    try {
        # Tries to get filename from Content-Disposition header
        if ($download.Headers["Content-Disposition"]) {
            $content = [System.Net.Mime.ContentDisposition]::new($download.Headers["Content-Disposition"])
            $installerFileName = $content.FileName
        }

        if ($installerFileName) {
            return $installerFileName
        }
    }
    catch {
    }

    # If the above is unsuccessful, get capture filename from download link
    #$matches = @()
    # uses GetFileName, decodes any HTTP encoding, removes the character '?' and preceeding characters, and matches it with a filename regex
    Add-Type -AssemblyName System.Web
    ([System.Web.HTTPUtility]::UrlDecode([System.IO.Path]::GetFileName($downloadLink)) -replace '\?.*$') -match '.+\..+$' | Out-Null

    if (!($matches[0])) {
        return "$global:programNameNoSpace.exe"
    }

    return $matches[0]

}
   
# Downloads installer
Function Get-Installer {
    Param (
        $DownloadLink,
        $installerFileName,
        $SavePath
    )
    $ProgressPreference = 'SilentlyContinue'

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # Tests if save path is existing
    if (!(Test-Path $savePath)) {
        Write-Log -LogType ERROR -Message "Download path $SavePath not existing. Please specify a valid path."
    }

    $attempts = 0
    $maxAttempts = 3
    #$null = $fileName

    # Downloads the file
    while ($attempts -lt $maxAttempts) {

        $attempts++

        Write-Log -LogType DEBUG -Message "Attempts: $attempts" 
        # Downloads the file
        try {
            $download = Invoke-WebRequest -Uri $downloadLink
        }
        catch [System.NotSupportedException] {
            Disable-IEFirstRunCustomization
            continue
        }
        catch {
            Write-Log -LogType ERROR -Message "Unable to download installer: $($Global:Error[0].exception.GetType().fullname)"
            #return $null
        }

        if (!$download) {
            Start-Sleep 5
            continue 
        }

        if (!$installerFileName) {
            $installerFileName = Get-InstallerFileName -Download $download
        }

        Write-Log -LogType DEBUG "Filename set to $installerFileName."
    
        # Actually saves the file to disk
        $installerPath = $SavePath + '\' + $installerFileName
        $f = [IO.File]::OpenWrite($installerPath); 
        try { 
            $download.RawContentStream.WriteTo($f); 
        }
        finally { 
            $f.Dispose(); 
        }

        if (Test-Path $installerPath) {
            return $installerPath
        }

        Start-Sleep 5
    }

    return $null
    
}

# Creates folder for storing installation files e.g. msi, exe, config files, etc
Function Add-InstallerFolder {
    Param (
        $Path
    )
    If (Test-Path -Path $Path) {
        try {
            Remove-Item -Path $Path -Force -Recurse -ErrorAction Stop
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to delete installer folder and its contents. Please investigate its cause and/or manually delete the folder: $($Global:Error[0])"
            return $null
        }
    }

    try {
        $installerFolder = New-Item -Path $Path -ItemType Directory
        return $installerFolder
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to create installer folder: $($Global:Error[0])"
        return $null
    }
}


#Deletes installer
Function Remove-InstallerFolder {
    Param (
        $Path,
        $CleanupDelay
    )

    Set-Location "C:\ProgramData\AppMani\ServiceCATRMM\work"

    $minimumPathLength = 4
    if ($Path.Length -lt $minimumPathLength) {
        Write-Log -LogType ERROR "Invalid path. As a security measure, this function needs a path with a minimum length of $minimumPathLength to proceed."
        return $null
    }

    # Cleans up installer folders
    Write-Log -LogType DEBUG -Message "Installation files cleanup will be performed in $cleanupDelay seconds."
    Start-Sleep -Seconds $CleanupDelay
    
    Write-Log -LogType INFO -Message "Cleaning up..." -NoNewline

    # Removes a file
    try {
        Remove-Item -Path $Path -Recurse -Force
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to delete installer folder: $($Global:Error[0])"
        return $null
    }

    Write-Log -LogType DEBUG -Message "Removed folder and contents of $Path." 
}

# Checks the registry for entries of the installed program and returns information about it
Function Get-InstalledProgram {
    Param (
        $RegistryDisplayName,

        [Switch]$All
    )
    
    if (!$RegistryDisplayName) {
        Write-Log -LogType ERROR -Message "Please specify a Registry DisplayName regex."
        return $null
    }

    $HKUPSDrive = Get-PSDrive HKU -ErrorAction SilentlyContinue
    if (!($HKUPSDrive)) {
        New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -Scope Script | Out-Null
    }

    # Write-Log -LogType INFO -Message "Searching registry for installations."

    $Apps = @()
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue # 32 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue # 64 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties" -ErrorAction SilentlyContinue
    $Apps += Get-ItemProperty "HKU:\*\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue
    
    $installedPrograms = $Apps | Where-Object DisplayName -match $RegistryDisplayName | Sort-Object { [System.Version]::Parse($_.DisplayVersion) } -Descending -ErrorAction Ignore

    $installedPrograms = Get-InstallationsArchitecture -InstalledPrograms $installedPrograms

    if (!$installedPrograms) {
        Write-Log -LogType DEBUG "No programs found." 
        return $null
    }

    if ($All) {
            
        return $installedPrograms
    }

    return $installedPrograms[0]
}

Function Get-MultipleInstalledProgram {
    Param (
        $RegistryDisplayName,
        $RegistryDisplayVersion
    )
    
    $HKUPSDrive = Get-PSDrive HKU -ErrorAction SilentlyContinue
    if (!($HKUPSDrive)) {
        New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -Scope Script | Out-Null
    }

    $Apps = @()
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" # 32 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"             # 64 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties"
    $Apps += Get-ItemProperty "HKU:\*\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    
    $installedPrograms = $Apps | Where-Object DisplayName -match $RegistryDisplayName

    if ($installedPrograms) {
        $installedPrograms = $Apps | Where-Object { ($_.DisplayName -match $RegistryDisplayName) -and ($_.DisplayVersion -match $RegistryDisplayVersion) }
        return $installedPrograms
    }
}

# Checks the registry for entries of the isntalled service and returns information about it
Function Get-InstalledService {
    Param (
        $ServiceDisplayName
    )
    # $RegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceDisplayName"

    # $installedService = Get-ItemProperty -Path $RegistryPath -ErrorAction SilentlyContinue

    # return $installedService

    $RegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\*"

    $installedService = Get-ItemProperty -Path $RegistryPath -ErrorAction SilentlyContinue | Where-Object DisplayName -eq $ServiceDisplayName

    return $installedService

}

# Gets exit codes to ignore when installing
Function Get-IgnoreExitCodes {
    Param (
        $ProgramName
    )

    $IgnoreExitCodes = @{
        'Adobe Digital Editions' = '1223'
    }

    $IgnoreExitCode = $IgnoreExitCodes.$ProgramName
    if (!$IgnoreExitCode) { return $null }
    else { return $IgnoreExitCode }
}

#Installs program using a one-liner msiexec or calls the installer executable with additional arguments
Function Install-Program {
    Param (
        $location,
        $installCommand
    )

    if (Test-Path $location) {
        Set-Location $location
    }
    else {
        Write-Log -LogType ERROR -Message "Unable to change location to $location."
    }

    Write-Log -LogType DEBUG -Message "Executing command $installCommand"

    try {
        cmd /c "$installCommand" > $null
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to install: $($Global:Error[0])"
        return 1
    }

    Write-Log -LogType INFO -Message "Execution completed with exit code $LASTEXITCODE"

    return $LASTEXITCODE
}

Function Confirm-ProgramInstallation {
    Param (
        $ProgramName, $registryDisplayName, $ProgramType
    )

    if (!$registryDisplayName) {
        $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName
    }

    if (!$ProgramType) {
        $ProgramType = 'Application'
    }
    
    # Loops X number of times to check registry keys for the program
    $tries = 1
    while ($tries -le 3) {
        Write-Log -LogType DEBUG -Message "Verifying installation. Tries: $tries"

        if ($ProgramType -eq 'Application') {
            $installedProgram = Get-InstalledProgram -RegistryDisplayName $RegistryDisplayName
        }
        else {
            $installedProgram = Get-InstalledService -ServiceDisplayName $programName
        }

        if ($null -ne $installedProgram) {
            return $installedProgram
        }
        
        Start-Sleep -s 15
        $tries++
    }
    Write-Log -LogType ERROR -Message "Script has reached the maximum number of retries on installation verification. Please investigate for issues."
    return $null
}

Function Confirm-ServiceInstallation {
    Param (
        $ServiceDisplayName
    )
    # Loops X number of times to check registry keys for the service
    $tries = 0
    while ($tries -le 3) {
        $tries++
        Write-Log -LogType DEBUG -Message "Verifying installation. Tries: $tries"

        $installedService = Get-InstalledService -ServiceDisplayName $ServiceDisplayName
        
        if ($null -ne $installedService) {
            return $installedService
        }

        Start-Sleep -s 15
    }
    Write-Log -LogType ERROR -Message "Script has reached the maximum number of retries on installation verification. Please investigate for issues."
    return $null
}

Function Get-VersionMatchRegex {
    Param (
        $ProgramName
    )

    $versionMatchRegexes = (
        @{
            ProgramName         = 'Agent Ransack'
            Regex               = '\d+\.\d+\.(\d+)\.\d+'
            FullVersionLocation = 'InstalledVersion'
        }
    )

    return ($versionMatchRegexes | Where-Object ProgramName -eq $ProgramName)
}

# Compares current version of a program from the registry and what's on the download link. There are programs that won't have registry entries and programs that won't have their versions on the download link, so please check first before using
Function Confirm-Update {
    Param (
        $ProgramName,
        $InstalledVersion,
        $LatestVersionAvailable
    )

    # # Sets a default regex if it's blank or null
    # if (!$VersionMatchRegex) { $VersionMatchRegex = '.*' }

    # # Removes whitespaces for programs who has a space on its Display Version for some reason like CutePDF: ' 4.1'
    # $InstalledVersion = $($InstalledVersion -replace '\s', '')
    
    # # Gets latest version available
    # # $latestVersion = $(Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber
    # if (!$LatestVersionAvailable) {
    # return $false
    # }

    # if ($LatestVersionAvailable -eq 'NO METHOD AVAILABLE') {
    # return $true
    # }

    Write-Log -LogType DEBUG -Message "Latest version available: $LatestVersionAvailable"
    Write-Log -LogType DEBUG -Message "Installed version: $InstalledVersion"

    # # Matches versions to a regex. This is for installed vs scraped latest versions that do not follow the same format e.g. 1.2.3456(registry display version) vs 1.2(scraped from website)
    # if (!($InstalledVersion -match $VersionMatchRegex)) {
    # Write-Log -LogType ERROR -Message "Installed version doesn't match version match regex."
    # return $false
    # }
    # else { $InstalledVersion = $matches[0] }

    # if (!($LatestVersionAvailable -match $VersionMatchRegex)) {
    # Write-Log -LogType ERROR -Message "Latest version doesn't match version match regex."
    # return $false
    # }
    # else { $LatestVersionAvailable = $matches[0] }

    $versionMatchRegex = Get-VersionMatchRegex -ProgramName $ProgramName
    if ($versionMatchRegex) {
        $regex = $versionMatchRegex.Regex

        if ($versionMatchRegex.FullVersionLocation -eq 'InstalledVersion') {
            if ( $InstalledVersion -match $regex ) {
                $InstalledVersion = $matches[1]
            }
            else {
                Write-Log -LogType ERROR -Message "Installed version doesn't match version match regex."
            }
            
        }
        elseif ($versionMatchRegex.FullVersionLocation -eq 'LatestVersion') {
            if ( $LatestVersionAvailable -match $regex ) {
                $LatestVersionAvailable = $matches[1]
            } 
            else {
                Write-Log -LogType ERROR -Message "Latest version doesn't match version match regex."
            }
        }
    }
    
    try {
        $isProgramForUpdate = [version]$InstalledVersion -lt [version]$LatestVersionAvailable
        if (!$isProgramForUpdate) {
            Write-Log -LogType INFO "Currently installed version is equal to or higher than latest version retrieved."
        }

        return $isProgramForUpdate

    }
    catch [System.Management.Automation.RuntimeException] {
        # Compares two the two versions not as version types
        $isProgramForUpdate = $InstalledVersion -lt $LatestVersionAvailable

        if (!$isProgramForUpdate) {
            Write-Log -LogType INFO "Currently installed version is equal to or higher than latest version retrieved."
        }

        return $isProgramForUpdate
    }

}

Function Set-RegistryItem {
    Param (
        $RegistryPath,
        $Name,
        $Value,
        $PropertyType
    )
    # Create the key if it does not exist
    If (-NOT (Test-Path $RegistryPath)) {
        try {
            New-Item -Path $RegistryPath -Force -ErrorAction Stop #| Out-Null
            Write-Log -LogType INFO -Message "Mew registry path $RegistryPath created."
        }
        catch {
            return $null
        }
    }
  
    # Now set the value
    try {
        New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force -ErrorAction Stop
        Write-Log -LogType INFO -Message "Registry item $RegistryPath\$Name set to $Value."
    }
    catch {
        return $null
    }
}

Function Get-ProgramArchitecture {
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $Program
    )

    $ProgramName = $Program.DisplayName
    switch -regex ($ProgramName) {
        "(64 bit|64\-bit|x64|64bit)" {
            # Write-Log -LogType INFO -Message "Architecture determined using DisplayName"
            return "x64"
        }
        "(32 bit|32\-bit|x86|32bit)" {
            # Write-Log -LogType INFO -Message "Architecture determined using DisplayName"
            return "x86"
        }
    }

    $InstallLocation = $Program.InstallLocation
    switch -regex ($InstallLocation) {
        "C:\\Program Files\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using InstallLocation"
            return "x64"
        }
        "C:\\Program Files \(x86\)\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using Program InstallLocation"
            return "x86"
        }
    }

    $UninstallString = $Program.UninstallString
    switch -regex ($UninstallString) {
        "C:\\Program Files\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using UninstallString"
            return "x64"
        }
        "C:\\Program Files \(x86\)\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using UninstallString"
            return "x86"
        }
    }

    if ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall') {
        # Write-Log -LogType INFO -Message "Architecture determined using PSParentPath"
        return "x86"
    }
    elseif ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') {
        # Write-Log -LogType INFO -Message "Architecture determined using PSParentPath"
        return "x64"
    }
    else {
        #Write-Log -LogType WARNING -Message "Unable to determine architecture."
        return $null
    }
}

Function Send-Keys {
    Param (
        $ApplicationWindowTitle,
        $Keys
    )

    $wshell = New-Object -ComObject wscript.shell;
    $wshell.AppActivate($ApplicationWindowTitle)
    $wshell.SendKeys($Keys)
}

Function Invoke-ModuleForUpdate {
    Param (
        $ModuleName
    )
    
    if (!(Get-PackageProvider -ListAvailable | Where-Object Name -eq 'Nuget')) {
        Write-Log -LogType INFO -Message "Installing Nuget..."
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Install-PackageProvider -Name Nuget -Force | Out-Null
    }
    Write-Log -LogType INFO -Message "Retrieving installed module..."
    $installedModule = Get-InstalledModule $ModuleName -ErrorAction SilentlyContinue

    # If not install module from PSGallery
    if ($null -eq $installedModule) {
        Write-Log -LogType INFO -Message "$ModuleName module not installed. Please install $ModuleName first."
    }
    # If module is installed check for updates and import
    else {            
        # Gets latest module version available in PSGallery
        $latestModuleVersion = Find-Module $ModuleName -ErrorAction Ignore
        if ($latestModuleVersion) {
                
            # Checks if installed module version needs an update
            if ($latestModuleVersion.Version -ne $installedModule.Version) {
                Write-Log -LogType INFO -Message "Installing new version of $ModuleName..." -NoNewline
                try {
                    Update-Module $ModuleName -Force -ErrorAction Stop
                    Write-Log -LogType INFO -Message "Done!"
                }
                catch {
                    Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                    return $null
                }
            }
            Else {
                Write-Log -LogType INFO -Message "Module $ModuleName is already up to date."
            }
        }

    }
}


Function Get-PreferredArchitecture {
    Param (
        $ProgramName
    )

    $PreferredArchitectures = @{
        'Adobe Acrobat' = 'x86'
    }

    $PreferredArchitecture = $PreferredArchitectures.$ProgramName
    if (!$PreferredArchitecture) { return 'x64' }
    else { return $PreferredArchitecture }
}

Function Resolve-ArchitectureSelection {
    Param (
        $ProgramName,
        $Architecture
    )

    $preferredArchitecture = Get-PreferredArchitecture -ProgramName $programName

    if (($Architecture -eq 'x64') -or ($Architecture -eq 'x86')) {
        Write-Log -LogType DEBUG -Message "Architecture set to $architecture."
    }

    elseif ($Architecture -eq 'Use existing') {
        Write-Log -LogType DEBUG -Message "'$Architecture' was selected for architecture."

        # Gets existing installations that we are able to get architectures from
        $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName
        $existingInstallation = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All | Where-Object Architecture -ne $null

        # If there are no installations detected architecture will be set null then the preferred will be used
        if (!$existingInstallation) {
            Write-Log -LogType DEBUG -Message "No existing installation of $programName found. Preferred architecture will be used."

            $architecture = $preferredArchitecture
            Write-Log -LogType DEBUG -Message "Architecture set to $architecture."
            return $Architecture

        }

        # Checks installations for conflicting architecture. If there are none, the script will use the architecture of the existing installations. If there is, the preferred architecture will be used.
        $Architecture = $existingInstallation[0].Architecture
        foreach ($installation in $existingInstallation) {
            if ($installation.Architecture -ne $architecture) {
                Write-Log -LogType DEBUG -Message "Installations of $programName with conflicting architecture detected. Preferred architecture will be used."
                    
                $architecture = $preferredArchitecture
                Write-Log -LogType DEBUG -Message "Architecture set to $architecture."
                return $Architecture
            }
        }    
    }
    
    # We set x64 as a default when a user-specified/preferred architecture is not present. This will step will only be done on programs where architecture can be selected.
    # elseif (($Architecture -eq 'preferred') -or (!$Architecture)) {
    elseif (($Architecture -eq 'preferred')) {
        
        Write-Log -LogType DEBUG -Message "'$Architecture' was selected for architecture."

        $Architecture = $preferredArchitecture
        Write-Log -LogType DEBUG -Message "Architecture set to $architecture."
    }

    else {
        Write-Log -LogType DEBUG -Message "'$Architecture' was selected for architecture."

        $Architecture = $preferredArchitecture
        Write-Log -LogType DEBUG -Message "Invalid selection. Preferred architecture $architecture will be used."
    }

    return $Architecture

}

Function Get-InstallerMetaData {
    Param (
        $ProgramName,
        $Architecture,
        $SavePath
    )

    $StorageAccountName = 'appmaniblob'
    $ContainerName = 'appmaniblobcontainer'

    $programNameNoSpace = $($($ProgramName -replace '\s', ''))

    $architectureTag = if ($Architecture) { "-$Architecture" }
    $InstallerMetaDataJsonName = $programNameNoSpace + $architectureTag
    $InstallerMetaDataJsonUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName/InstallerMetadata/$InstallerMetaDataJsonName.json"
    $InstallerMetaDataJsonPath = "$SavePath\$InstallerMetaDataJsonName-InstallerMetaData.json"

    Write-Log -LogType DEBUG "Installer metadata URL: $installerMetaDataJsonUrl"


    try {
        Invoke-WebRequest -Uri $installerMetaDataJsonUrl -OutFile $installerMetaDataJsonPath -ErrorAction Stop
    }
    catch {
        Write-Log -LogType DEBUG -Message "Unable to download $programNameNoSpace.json: $($Global:Error[0])"

        if ($architectureTag) {
            
            Write-Log -LogType INFO -Message "Retrying installer metadata retrieval without specific architecture."
            $InstallerMetaDataJsonUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName/InstallerMetadata/$programNameNoSpace.json"
            Write-Log -LogType DEBUG "Installer metadata URL: $installerMetaDataJsonUrl"
        }
        else {
            $preferredArchitecture = Get-PreferredArchitecture -ProgramName $ProgramName
            Write-Log -LogType DEBUG -Message "Architecture might have needed to be specified. Retrying installer metadata retrieval using preferred architecture."

            $InstallerMetaDataJsonUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName/InstallerMetadata/$programNameNoSpace-$preferredArchitecture.json"
            Write-Log -LogType DEBUG "Installer metadata URL: $installerMetaDataJsonUrl"
        }

        $retry = $true
    }

    if ($retry -eq $true) {

        try {
            Invoke-WebRequest -Uri $installerMetaDataJsonUrl -OutFile $installerMetaDataJsonPath -ErrorAction Stop
        }
        catch {
            Write-Log -LogType ERROR -Message "Unable to download $programNameNoSpace.json: $($Global:Error[0])"

            return $null
        }
        
    }

    try {
        $installerMetaData = Get-Content -Path $installerMetaDataJsonPath -ErrorAction Stop | ConvertFrom-Json
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to retrieve contents of $installerMetaDataJsonPath : $($Global:Error[0])"
        return $null
    }
    
    if (!$installerMetaData) {
        Write-Log -LogType ERROR -Message "installerData.json is empty."
    }

    Write-Log -LogType DEBUG -Message "Successfully retrieved installer metadata."

    return $installerMetaData
}

Function Get-DownloadLink {
    Param (
        $ProgramName,
        $Architecture
    )

    $null = $Global:ArchitectureUsed

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    $attempts = 1
    $maxAttempts = 3

    while ($attempts -le $maxAttempts) {
        Write-Log -LogType DEBUG -Message "Attempts: $attempts"
        try {
            switch ($ProgramName) {
                '7-zip' {
                        
                    $HomePage = "https://www.7-zip.org/"

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $architectureFilter = '-x64'
                    }
                    else {
                        $architectureFilter = ''
                    }
            
                    $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html'
                    $Pattern = '<A href=\"(?<link>a/7z\d+{0}\.exe)\">Download</A>' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $link = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                
                    if ($null -ne $link) {
                        $downloadLink = $HomePage + "$($link)"
                        #$Global:ArchitectureUsed =
                    }
                    else {
                        Write-Log -LogType ERROR -Message "Version requested is not available."
                        return $null
                    }
                    break
                }
                'Actionstep Office Add-In' {
                    $downloadLink = ((Invoke-WebRequest 'https://www.actionstep.com/integrations/microsoft-office-for-windows/' -UseBasicParsing).Links | Where-Object class -eq 'install-link').href
                    break
                }
                'Adobe Acrobat' {
                    # $versionOverride = '22.003.20282'
            
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $downloadLinkFormat = 'https://ardownload2.adobe.com/pub/adobe/acrobat/win/AcrobatDC/{0}/'
                        $fileNameFormat = 'AcroRdrDCx64{0}_en_US.exe'
                    }
                    elseif ($architecture -eq 'x86') {
                        $downloadLinkFormat = 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/{0}/'
                        $fileNameFormat = 'AcroRdrDC{0}_en_US.exe'
                        
                    }
            
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber

                    if ($versionOverride) {
                        Write-Log -LogType DEBUG -Message "Version override detected: $versionOverride"
                        $latestVersion = $versionOverride
                    }

                    # If no latest version was retrieved and no override, return null
                    if (!$latestVersion) {
                        Write-Log -LogType INFO -Message "Unable to retrieve download link."
                        return $null
                    }
                
                    $latestVersion = $latestVersion -replace '[.]', ''
                    
                    $filenameFormat = $filenameFormat -f $latestVersion
                    $downloadLink = $("$downloadLinkFormat" + "$filenameFormat") -f $latestVersion
                    break
                }
                'Adobe Digital Editions' {
                    #$HTML = Invoke-RestMethod 'https://www.adobe.com/nz/solutions/ebook/digital-editions/download.html' -Headers @{"accept" = "*/*" } -ErrorAction Stop
                    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                    $HTML = Invoke-WebRequest -UseBasicParsing -Uri "https://www.adobe.com/solutions/ebook/digital-editions/download.html" -WebSession $session -Headers @{  "accept-encoding" = "gzip, deflate, br"; "accept-language" = "en-US,en;q=0.9,fil;q=0.8" }
                    $Pattern = '<a href=\"(?<link>.+?Installer\.exe)\">'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Advanced IP Scanner' {
                    $HTML = Invoke-RestMethod 'https://www.advanced-ip-scanner.com/download/'
                    $Pattern = '<a href=\"(?<link>https://download\.advanced-ip-scanner\.com/download/files/Advanced_IP_Scanner_(?<version>.+?)\.exe)"' 
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Ajax PRO Desktop' {
                    $downloadLink = 'https://desktop.ajax.systems/app/setup/release/windows/AjaxSetup.exe'
                    break
                }
                'Arcserve ShadowControl' {
                    $downloadLink = ((Invoke-WebRequest 'https://www.arcserve.com/software-downloads/shadowprotect').Links | Where-Object href -like '*ShadowControl_Installer_*_en.msi').href | Select-Object -First 1
                    break
                }
                'Arcserve ShadowProtect' {
                    
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $architectureFilter = 'win64'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = 'win32'
                    }
                    
                    $downloadLink = ((Invoke-WebRequest 'https://www.arcserve.com/software-downloads/shadowprotect').Links | Where-Object href -like ('*ShadowProtect_SPX-*.{0}.msi' -f $architectureFilter)).href | Select-Object -First 1
                    break
                }
                'Audacity' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '64bit'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = '32bit'
                    }
                    
                    # $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/'
                    # $Pattern = '<a href=\"(?<link>.*)\">Audacity .+? {0} installer</a>' -f $architectureFilter
                    # $AllMatches = ([regex]$Pattern).Matches($HTML)
                    # $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $latestVersionNumber = Get-LatestVersionNumber -ProgramName 'Audacity'
                    $downloadLink = 'https://github.com/audacity/audacity/releases/download/Audacity-{0}/audacity-win-{0}-{1}.exe' -f $latestVersionNumber.VersionNumber, $architectureFilter
                    
                    break
                }
                'BarTender' {
                    $downloadLink = (Invoke-WebRequest -Uri "https://portal.seagullscientific.com/downloads/bartender").links | Where-Object href -like '*.exe' | Select-Object href -First 1
                    $downloadLink = $downloadLink.href
                    break
                }
                'Bitwarden' {
                    $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Citrix Workspace' {
                    $downloadLink = ((Invoke-WebRequest -URI 'https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html').Links | Where-Object { ($_.outerText -like 'Download *') -and ($_.rel -like '*CitrixWorkspaceApp.exe*') }).rel[0]
                    $downloadLink = "https:" + $downloadLink
                    break
                }
                'CutePDF Writer' {
                    $downloadLink = 'https://www.cutepdf.com/download/CuteWriter.exe'
                    break
                }
                'DC Loader' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/digital-certificates-and-security/download-or-renew-your-two-year-digital-certificate'
                    $Pattern = '<p><a class=\"button\" href=\"(?<link>.*)\">Download DC Loader.+?</a></p>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Digisign' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Digisign.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break    
                }
                'Digisign Repair Tool' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Digisign Repair Tool.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Dropbox' {
                    $latestVersionNumber = (Get-LatestVersionNumber -ProgramName Dropbox).VersionNumber
                    # $originalLink = 'https://www.dropbox.com/download?full=1&plat=win'
                    $originalLink = 'https://www.dropbox.com/download?build={0}&full=1&plat=win' -f $latestVersionNumber
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Microsoft Edge' {
                    $originalLink = 'https://go.microsoft.com/fwlink/?linkid=2109047&Channel=Stable&language=en&consent=1'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Filezilla' {
            
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $architectureFilter = 'win64'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = 'win32'
                    }
                        
                    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                    $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
                    $HTML = Invoke-RestMethod -UseBasicParsing -Uri "https://filezilla-project.org/download.php?show_all=1" -WebSession $session                    
                    $Pattern = '<a href=\"(?<link>.*)\" rel="nofollow">FileZilla_.+?_{0}-setup.exe</a>' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    
                    break
                }
                'Foxit PDF Reader' {
                    $originalLink = 'https://www.foxit.com/downloads/latest.html?product=Foxit-Reader&platform=Windows&version=&package_type=&language=English&distID='
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Google Drive' {
                    $downloadLink = 'https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe'
                    break     
                }
                'GPL Ghostscript' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') {
                        $architectureFilter = 'w64'
                    }
                    else {
                        $architectureFilter = 'w32'
                    }
                        
                    #$link = 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/latest'
                    #$latestLink = (Invoke-WebRequest -Uri $link -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    # $latestLink -match 'gs(.+?)$' | Out-Null
                    #$downloadLink = $latestLink.replace('tag', 'download') + "/gs$($matches[1])$architectureFilter.exe"
                    
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber.Replace(".", "")
                    $downloadLink = 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs{0}/gs{0}{1}.exe' -f $latestVersion, $architectureFilter
                    break
                }
                'Google Chrome' {
                    $downloadLink = 'http://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi'
                    break
                }
                'HP Support Assistant' {
                    $downloadLink = 'https://ftp.ext.hp.com/pub/softpaq/sp141501-142000/sp141886.exe'
                    break
                }
                'IrfanView' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    if ($Architecture -eq 'x64') {
                        $ArchitectureFilter = 'x64'
                    }
                    
                    $test = (Invoke-WebRequest -Uri "https://www.fosshub.com/IrfanView.html" -UseBasicParsing).content 
                    #$data = ($test | Select-String -Pattern '(?<=\s=).*').matches.value | ConvertFrom-Json
                    $test -match 'var settings =(.+?)\n' | Out-Null
                    $data = $matches[1] | ConvertFrom-Json

                    try { 
                        $Url = 'https://api.fosshub.com/download' 
                        $Params = @{ 
                            Uri             = $Url 
                            Body            = @{ 
                                projectId  = "$($data.projectId)" 
                                releaseId  = "$($data.pool.f.r | Select -Unique)" 
                                projectUri = 'IrfanView.html' 
                                fileName   = $((($data).pool.f | Where-Object { $_.n -match ('iview(\d+)_?{0}_setup\.exe' -f $ArchitectureFilter) }))[0].n
                                source     = "$($data.pool.c)" 
                            }
                            Headers         = @{
                                'User-Agent' = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                            }
                            Method          = 'POST'
                            UseBasicParsing = $true
                        }
                        $info = (Invoke-WebRequest @Params).Content | ConvertFrom-Json
                        $Global:ErrorType = $Response.error
                        if ($Global:ErrorType -ne $Null) {
                            throw "ERROR RETURNED $Global:ErrorType"
                            return $Null
                        }
                        $downloadLink = ($info.data)[0].url
                    }
                    catch {
                        Write-Error $_
                    }

                    break
                }
                'Jabra Direct' {
                    $downloadLink = 'https://jabraxpressonlineprdstor.blob.core.windows.net/jdo/JabraDirectSetup.exe'
                    break
                }
                'Java 8' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    # if ($architecture -eq 'x64') {
                    # $architectureFilter = '\(64-bit\)'
                    # }
                    # else {
                    # $architectureFilter = 'Offline'
                    # }
            
                    # $URL = "https://www.java.com/en/download/manual.jsp"
                    # $global:ie = New-Object -com "InternetExplorer.Application"
                    # $global:ie.visible = $false
                    # $global:ie.Navigate($URL)
            
                    # DO { Start-Sleep -s 1 }UNTIL(!($global:ie.Busy))
                    # Start-Sleep 5
                    # if ($global:ie.Document.body.innerHTML) {
                    # $HTML = $global:ie.Document.body.innerHTML.ToString()
                    # }
                    # else { return $null }
                    
                    # $Pattern = '<a href=\"(?<link>.*)\" title=\"Download Java software for Windows {0}\">Windows Offline' -f $architectureFilter
                    # $AllMatches = ([regex]$Pattern).Matches($HTML)
                    # $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    if ($architecture -eq 'x64') {
                        $downloadLink = 'https://javadl.oracle.com/webapps/download/AutoDL?BundleId=248242_ce59cff5c23f4e2eaf4e778a117d4c5b'
                    }
                    else {
                        $downloadLink = 'https://javadl.oracle.com/webapps/download/AutoDL?BundleId=248240_ce59cff5c23f4e2eaf4e778a117d4c5b'
                    }
    
                    break
                }
                'LegalAid Templates' {
                    $domainName = 'https://www.justice.govt.nz'
                    $HTML = Invoke-RestMethod "$domainName/about/lawyers-and-service-providers/legal-aid-lawyers/forms/download-word-template-package/"
                    $Pattern = '<a title=\"LegalAid Templates Version \d+ installer\" href=\"(?<link>.*)\">Word'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    $downloadLink = $domainName + $downloadLink
                    break
                }
                'LOLComponents' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class="button" href=\"(?<link>.*)\">Landonline Client Components \(ZIP .+?\)</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Microsoft 365' {
                    $HTML = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
                    $Pattern = '<td class=\"file-link\"><a href=\"(?<link>.*)\"><span'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Mozilla Firefox' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                    }
                    elseif ($architecture -eq 'x86') {
                        $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win&lang=en-US'
                    }
                    
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Net Monitor for Employees Agent' {
                    $downloadLink = 'https://networklookout.com/dwn/nmemplpro_agent.msi'                    
                    break
                }
                'Nitro PDF Pro' {
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture
                    $majorVersion = ($latestVersion.Split("."))[0]
                    $downloadLink = 'https://downloads.gonitro.com/professional_{0}/en/nls/nitro_pro{1}_{2}.msi' -f $latestVersion, $majorVersion, $architecture
                    break
                }
                'Notepad++' {
                        
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    $NPlusPlusWebsite = "https://notepad-plus-plus.org"
                    $DownloadPage = $NPlusPlusWebsite + "/downloads"
                    
                    # $filter = '*Installer.exe'
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '*Installer.x64.exe'
                    }
                    else {
                        $architectureFilter = '*Installer.exe'
                    }
                    
                    try {
                        $currentVersion = $($(Invoke-WebRequest -Uri $DownloadPage).Links | Where-Object innerText -like 'Current Version*').href
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                    
                    if ($null -ne $currentVersion) {
                        $NPlusPlusCurrentVersionDownloadPage = $NPlusPlusWebsite + $currentVersion
                        
                        try {
                            $downloadLink = $(Invoke-WebRequest $NPlusPlusCurrentVersionDownloadPage).Links.href -like $architectureFilter | Select-Object -First 1
                            #return $downloadLink
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    }
                    else {
                        Write-Log -LogType ERROR -Message "Unable to find current version."
                        return $null
                    }

                    break
                }
                'Patriot Task Service' {
                    $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat611182Stable.zip'
                    # $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat610.zip'
                    break
                }
                'Patriot Version 6 Client' {
                    $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat611182Stable.zip'
                    # $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat610.zip'
                    break
                }
                'Patriot Reporting Components' {
                    $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat611182Stable.zip'
                    # $downloadLink = 'https://ezpsa.com/downloads/patriot/UpdPat610.zip'
                    break
                }
                'Patriot VLC Extension' {
                    $downloadLink = 'https://www.patriotsystems.com/downloads/Patriot_VLC_Extension_3.5.1.0.exe'
                    break
                }
                'PDFCreator' {
                    $originalLink = 'https://download.pdfforge.org/download/pdfcreator/PDFCreator-stable?download'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Philips Device Connector' {
                    $originalLink = 'https://www.dictation.philips.com/pcl8000/nativehostinstaller_win/'
                    $downloadLink = 'https://www.dictation.philips.com'
                    $downloadLink += (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'Putty' {
                    $PuttyDownloadPage = "https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html"
            
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') { $ArchitectureFilter = 'w64' }
                    elseif ($Architecture -eq 'x86') { $ArchitectureFilter = 'w32' }
            
                    try {
                        $links = (Invoke-WebRequest $PuttyDownloadPage).Links.href 
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
            
                    if ($links) {
                        $downloadLink = $links | Where-Object { ($_ -match "$ArchitectureFilter/(.+?)-installer\.msi$") }
                    }
                    else {
                        Write-Log -LogType ERROR -Message "No links found."
                        return $null
                    }
            
                    break
                }
                'Python 3' {
                    
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    $downloadLink = $(Invoke-WebRequest -Uri 'https://www.python.org/downloads/').Links.href | Where-Object { $_ -like '*.exe' }
                    if ($architecture -eq 'x86') {
                        $downloadLink = $downloadLink -replace "-amd64", ""
                    }

                    break
                }
                'Remote Access Tool' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Remote Access Installer.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    break
                }
                'Snagit' {
                    $latestVersionID = (Invoke-RestMethod 'https://www.techsmith.com/api/v/1/products/getallversions/12')[0].VersionID
                    $downloadInformation = (Invoke-RestMethod "https://www.techsmith.com/api/v/1/products/getversioninfo/$latestVersionID").PrimaryDownloadInformation
                    $downloadLink = "https://download.techsmith.com" + $downloadInformation.RelativePath + "snagit.msi"
                    break
                }
                'ServiceCATRMM' {
                    $downloadLink = 'https://ezpsa.com/downloads/scaudit/ServiceCatSetup.exe'
                    break
                }
                'Synology Drive Client' {
                    $downloadLinkFormat = 'https://global.download.synology.com/download/Utility/SynologyDriveClient/{0}/Windows/Installer/i686/Synology%20Drive%20Client-{0}.msi'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'
                    $downloadLink = $downloadLinkFormat -f $latestVersion
                    break
                }
                'Synology Active Backup for Business Agent' {
                    
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') { $architectureFilter = 'x86_64' }
                    elseif ($Architecture -eq 'x86') { $architectureFilter = 'i686' }
                    
                    $downloadLinkFormat = 'https://global.synologydownload.com/download/Utility/ActiveBackupBusinessAgent/{0}/Windows/{1}/Synology%20Active%20Backup%20for%20Business%20Agent-{0}-{2}.msi?model=DS220%2B&bays=2&dsm_version=7.1.1'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'
                    $downloadLink = $downloadLinkFormat -f $latestVersion, $architectureFilter, $architecture
                    break
                }
                'Sysmon64' {
                    $downloadLink = 'https://download.sysinternals.com/files/Sysmon.zip'
                    break
                }
                'Microsoft Teams' {
                    $originalLink = 'https://teams.microsoft.com/downloads/desktopcontextualinstaller?env=prod&intent=work&plat=windows&download=true'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    break
                }
                'TreeSize Free' {
                    $downloadLink = 'https://downloads.jam-software.de/treesize_free/TreeSizeFreeSetup.exe'
                    break
                }
                'Trend Micro Security Agent' {
                    $downloadLink = 'https://ezpsa.com/downloads/files/WFBS-SVC_Agent_Installer.msi'
                    break
                }
                'UniPrint' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    $regex = "UniPrintClientMSI_\d+_$architecture.zip$"
                    $downloadLink = (Invoke-WebRequest -Uri 'https://www.uniprint.net/en/uniprint-client/' -UseBasicParsing).Links.href | Where-Object { $_ -match $regex }
                    break
                }
                'VLC' {
                    
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture
                    
                    $VLCDownloadPage = "https://www.videolan.org/vlc/download-windows.html"
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '*win64.exe'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = '*win32.exe'
                    }
                        
                    try {
                        $versionDownloadPage = $(Invoke-WebRequest -Uri $VLCDownloadPage).Links | Where-Object href -like $architectureFilter
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                        
                    $versionDownloadPage = "https:" + $versionDownloadPage.href
                    try {
                        $downloadLink = $((Invoke-WebRequest -Uri $versionDownloadPage).Links | Where-Object href -like $architectureFilter | Select-Object -First 1).href
                        #return $downloadLink
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                    break
                }
                'Windirstat' {
                    
                    $test = (Invoke-WebRequest -Uri "https://www.fosshub.com/WinDirStat.html" -UseBasicParsing).content 
                    #$data = ($test | Select-String -Pattern '(?<=\s=).*').matches.value | ConvertFrom-Json
                    $test -match 'var settings =(.+?)\n' | Out-Null
                    $data = $matches[1] | ConvertFrom-Json

                    try { 
                        $Url = 'https://api.fosshub.com/download' 
                        $Params = @{ 
                            Uri             = $Url 
                            Body            = @{ 
                                projectId  = "$($data.projectId)" 
                                releaseId  = "$($data.pool.f.r | Select-Object -Unique)" 
                                projectUri = 'WinDirStat.html' 
                                fileName   = $((($data).pool.f | Where-Object { $_.n -match ('windirstat1_1_2_setup\.exe') }))[0].n
                                source     = "$($data.pool.c)" 
                            }
                            Headers         = @{
                                'User-Agent' = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                            }
                            Method          = 'POST'
                            UseBasicParsing = $true
                        }
                        $info = (Invoke-WebRequest @Params).Content | ConvertFrom-Json
                        $Global:ErrorType = $Response.error
                        if ($Global:ErrorType -ne $Null) {
                            throw "ERROR RETURNED $Global:ErrorType"
                            return $Null
                        }
                        $downloadLink = ($info.data)[0].url
                    }
                    catch {
                        Write-Error $_
                    }

                    break
                }
                'WinRAR' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $architectureFilter = 'x64'

                    if ($architecture -eq 'x86') {
                        $architectureFilter = 'x32'
                    }

                    $regex = '{0}-\d+\.exe' -f $architectureFilter
                    $href = ((Invoke-WebRequest https://www.rarlab.com/download.htm).Links | Where-Object href -match $regex | Select-Object href -First 1).href
                    $downloadLink = 'https://www.rarlab.com' + $href

                    break
                }
                'WinSCP' {
                    $homePage = 'https://winscp.net/'
                    $HTML = Invoke-RestMethod 'https://winscp.net/eng/download.php'
                    $Pattern = '<a href=\"(?<link>.*)\" class=.+?>Download <strong>WinSCP</strong> .+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = $homePage + ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $downloadLink = Get-RedirectedUrl -URL $downloadLink -Counter 3

                    break
                }
                'Wireshark' {

                    $wiresharkDownloadPage = "https://www.wireshark.org/download.html"
            
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    if ($Architecture -eq 'x86') {
                        Write-Log -LogType INFO "32-bit is no longer available on the latest version. The 64-bit version will be downloaded."
                        #$architectureFilter = 'win32'
                        $Architecture = 'x64'
                        $Global:ArchitectureUsed = 'x64'
                    }

                    $latestVersion = (Get-LatestVersionNumber -ProgramName Wireshark).VersionNumber
            
                    $webRequest = Invoke-WebRequest -Uri $WiresharkDownloadPage
                    $downloadLink = $webRequest.Links.href | Where-Object { $_ -like "*-$latestVersion-$architecture.exe" }

                    break
                }
                'Yealink USB Connect' {
                    $downloadLink = ((Invoke-WebRequest 'https://www.yealink.com/en/product-detail/usb-connect-management' -UseBasicParsing).Links | Where-Object href -like '*yealink-usb-connect-*.msi').href
                    break
                }
                'ZelloWork' {
                    $downloadLink = 'https://www.zello.com/data/mesh/ZelloWorkClient.msi'
                    break
                }
                'Zoom' {

                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    $Global:ArchitectureUsed = $Architecture

                    $architectureFilter = ''

                    if ($architecture -eq 'x64') {
                        $architectureFilter = '?archType=x64'
                    }

                    $downloadLink = 'http://zoom.us/client/latest/ZoomInstallerFull.msi{0}' -f $architectureFilter
                    break
                }
                Default {
                    Write-Log -LogType INFO -Message "No matching function to retrieve download link for $programName"
                    return $null
                }
            }
        }
        catch [System.NotSupportedException] {
            Disable-IEFirstRunCustomization
            continue
        }
        catch {
            Write-Log -LogType ERROR "Unable to retrieve download link for $ProgramName. $($Global:Error[0])"
        }

        if ($downloadLink) {
            return $downloadLink
        }

        $attempts++
        Start-Sleep 10
    }

    Write-Log -LogType ERROR -Message "The maximum number of attempts to retrieve the download link has been reached."
    return $null
}

Function Get-DownloadLinkV2 {
    Param (
        $ProgramName
    )

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    $attempts = 1
    $maxAttempts = 3

    while ($attempts -le $maxAttempts) {
        Write-Log -LogType DEBUG -Message "Attempts: $attempts"
        try {
            switch ($ProgramName) {
                '7-zip' {
                        
                    $HomePage = "https://www.7-zip.org/"
            
                    $HTML = Invoke-RestMethod 'https://www.7-zip.org'

                    $x64Pattern = '<A href=\"(?<link>a/7z\d+{0}\.exe)\">Download</A>' -f '-x64'
                    $AllMatches = ([regex]$x64Pattern).Matches($HTML)
                    $x64DownloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $x86Pattern = '<A href=\"(?<link>a/7z\d+\.exe)\">Download</A>'
                    $AllMatches = ([regex]$x86Pattern).Matches($HTML)
                    $x86DownloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $HomePage + $x64DownloadLink
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $HomePage + $x86DownloadLink
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Actionstep Office Add-In' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = ((Invoke-WebRequest 'https://www.actionstep.com/integrations/microsoft-office-for-windows/' -UseBasicParsing).Links | Where-Object class -eq 'install-link').href
                    }

                    break
                }
                'Adobe Acrobat' {
                    # $versionOverride = '22.003.20282'
            
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber

                    if ($versionOverride) {
                        Write-Log -LogType DEBUG -Message "Version override detected: $versionOverride"
                        $latestVersion = $versionOverride
                    }

                    # If no latest version was retrieved and no override, return null
                    if (!$latestVersion) {
                        Write-Log -LogType INFO -Message "Unable to retrieve download link."
                        return $null
                    }
                
                    $latestVersion = $latestVersion -replace '[.]', ''

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://ardownload2.adobe.com/pub/adobe/acrobat/win/AcrobatDC/{0}/AcroRdrDCx64{0}_en_US.exe' -f $latestVersion
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/{0}/AcroRdrDC{0}_en_US.exe' -f $latestVersion
                            Architecture = 'x86'
                        }
                    )

                    
                    break
                }
                'Adobe Digital Editions' {
                    #$HTML = Invoke-RestMethod 'https://www.adobe.com/nz/solutions/ebook/digital-editions/download.html' -Headers @{"accept" = "*/*" } -ErrorAction Stop
                    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                    $HTML = Invoke-WebRequest -UseBasicParsing -Uri "https://www.adobe.com/solutions/ebook/digital-editions/download.html" -WebSession $session -Headers @{  "accept-encoding" = "gzip, deflate, br"; "accept-language" = "en-US,en;q=0.9,fil;q=0.8" }
                    $Pattern = '<a href=\"(?<link>.+?Installer\.exe)\">'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Advanced IP Scanner' {
                    $HTML = Invoke-RestMethod 'https://www.advanced-ip-scanner.com/download/'
                    $Pattern = '<a href=\"(?<link>https://download\.advanced-ip-scanner\.com/download/files/Advanced_IP_Scanner_(?<version>.+?)\.exe)"' 
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Agent Ransack' {
                    $downloadLink = ((Invoke-WebRequest 'https://www.mythicsoft.com/agentransack/download/').Links | Where-Object href -match 'agentransack_(\d+).exe').href | Select -First 1
                                        
                    $downloadLink = [PSCustomObject]@{
                        URL = "https:" + $downloadLink
                    }

                    break
                }
                'Ajax PRO Desktop' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://desktop.ajax.systems/app/setup/release/windows/AjaxSetup.exe'
                    }

                    break
                }
                'Arcserve ShadowControl' {

                    $downloadLink = [PSCustomObject]@{
                        URL = ((Invoke-WebRequest 'https://www.arcserve.com/software-downloads/shadowprotect').Links | Where-Object href -like '*ShadowControl_Installer_*_en.msi').href | Select-Object -First 1
                    }

                    break
                }
                'Arcserve ShadowProtect' {
 
                    $Links = (Invoke-WebRequest 'https://www.arcserve.com/software-downloads/shadowprotect').Links

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = ($Links | Where-Object href -like ('*ShadowProtect_SPX-*.{0}.msi' -f 'win64')).href | Select-Object -First 1
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = ($Links | Where-Object href -like ('*ShadowProtect_SPX-*.{0}.msi' -f 'win32')).href | Select-Object -First 1
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Audacity' {

                    $latestVersionNumber = Get-LatestVersionNumber -ProgramName 'Audacity'
                    
                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://github.com/audacity/audacity/releases/download/Audacity-{0}/audacity-win-{0}-{1}.exe' -f $latestVersionNumber.VersionNumber, '64bit'
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://github.com/audacity/audacity/releases/download/Audacity-{0}/audacity-win-{0}-{1}.exe' -f $latestVersionNumber.VersionNumber, '32bit'
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'BarTender' {

                    $downloadLink = [PSCustomObject]@{
                        URL = ((Invoke-WebRequest -Uri "https://portal.seagullscientific.com/downloads/bartender").links | Where-Object href -like '*.exe' | Select-Object href -First 1).href
                    }

                    break
                }
                'Bitwarden' {
                    $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows'

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Citrix Workspace' {
                    $downloadLink = ((Invoke-WebRequest -URI 'https://www.citrix.com/downloads/workspace-app/workspace-app-for-windows-long-term-service-release/workspace-app-for-windows-LTSR-Latest.html').Links | Where-Object { ($_.outerText -like 'Download *') -and ($_.rel -like '*CitrixWorkspaceApp.exe*') }).rel[0]

                    $downloadLink = [PSCustomObject]@{
                        URL = "https:" + $downloadLink
                    }

                    break
                }
                'CutePDF Writer' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.cutepdf.com/download/CuteWriter.exe'
                    }
                    break
                }
                'DC Loader' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/digital-certificates-and-security/download-or-renew-your-two-year-digital-certificate'
                    $Pattern = '<p><a class=\"button\" href=\"(?<link>.*)\">Download DC Loader.+?</a></p>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Digisign' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Digisign.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break    
                }
                'Digisign Repair Tool' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Digisign Repair Tool.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Dropbox' {
                    $latestVersionNumber = (Get-LatestVersionNumber -ProgramName Dropbox).VersionNumber
                    # $originalLink = 'https://www.dropbox.com/download?full=1&plat=win'
                    $originalLink = 'https://www.dropbox.com/download?build={0}&full=1&plat=win' -f $latestVersionNumber

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Microsoft Edge' {
                    $originalLink = 'https://go.microsoft.com/fwlink/?linkid=2109047&Channel=Stable&language=en&consent=1'

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Filezilla' {
                        
                    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                    $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
                    $HTML = Invoke-RestMethod -UseBasicParsing -Uri "https://filezilla-project.org/download.php?show_all=1" -WebSession $session

                    $x64Pattern = '<a href=\"(?<link>.*)\" rel="nofollow">FileZilla_.+?_{0}-setup.exe</a>' -f 'win64'
                    $AllMatches = ([regex]$x64Pattern).Matches($HTML)
                    $x64DownloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $x86Pattern = '<a href=\"(?<link>.*)\" rel="nofollow">FileZilla_.+?_{0}-setup.exe</a>' -f 'win32'
                    $AllMatches = ([regex]$x86Pattern).Matches($HTML)
                    $x86DownloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    
                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $x64DownloadLink
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $x86DownloadLink
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Foxit PDF Reader' {
                    $originalLink = 'https://www.foxit.com/downloads/latest.html?product=Foxit-Reader&platform=Windows&version=&package_type=&language=English&distID='

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Github Desktop' {
                    $originalLink = 'https://central.github.com/deployments/desktop/desktop/latest/win32'
                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Google Drive' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe'
                    }

                    break     
                }
                'GPL Ghostscript' {
                    
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber.Replace(".", "")

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs{0}/gs{0}{1}.exe' -f $latestVersion, 'w64'
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs{0}/gs{0}{1}.exe' -f $latestVersion, 'w32'
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Google Chrome' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'http://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi'
                    }

                    break
                }
                'HP Support Assistant' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://ftp.ext.hp.com/pub/softpaq/sp141501-142000/sp141886.exe'
                    }

                    break
                }
                'IrfanView' {

                    Function Get-URLFromFosshub {
                        Param (
                            $ArchitectureFilter,
                            $Data
                        )

                        try { 
                            $Url = 'https://api.fosshub.com/download' 
                            $Params = @{ 
                                Uri             = $Url 
                                Body            = @{ 
                                    projectId  = "$($Data.projectId)" 
                                    releaseId  = "$($Data.pool.f.r | Select -Unique)" 
                                    projectUri = 'IrfanView.html' 
                                    fileName   = $((($Data).pool.f | Where-Object { $_.n -match ('iview(\d+)_?{0}_setup\.exe' -f $ArchitectureFilter) }))[0].n
                                    source     = "$($Data.pool.c)" 
                                }
                                Headers         = @{
                                    'User-Agent' = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                                }
                                Method          = 'POST'
                                UseBasicParsing = $true
                            }
                            
                            $info = (Invoke-WebRequest @Params).Content | ConvertFrom-Json
                            
                            $Global:ErrorType = $Response.error
                            if ($Global:ErrorType -ne $Null) {
                                throw "ERROR RETURNED $Global:ErrorType"
                                return $null
                            }

                            return ($info.data)[0].url
                        }
                        catch {
                            Write-Error $_
                        }                    
                    }

                    $webRequest = (Invoke-WebRequest -Uri "https://www.fosshub.com/IrfanView.html" -UseBasicParsing).content 
                    #$data = ($test | Select-String -Pattern '(?<=\s=).*').matches.value | ConvertFrom-Json
                    $webRequest -match 'var settings =(.+?)\n' | Out-Null
                    $data = $matches[1] | ConvertFrom-Json
                    
                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = Get-URLFromFosshub -ArchitectureFilter 'x64' -Data $data
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = Get-URLFromFosshub -Data $data
                            Architecture = 'x86'
                        }
                    )
                    

                    break
                }
                'ISQLME' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'http://software.nzcs.co.nz/downloads/ISQLMESetup.exe'
                    }

                    break
                }
                'Jabra Direct' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://jabraxpressonlineprdstor.blob.core.windows.net/jdo/JabraDirectSetup.exe'
                    }

                    break
                }
                'Java 8' {

                    # $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture

                    # $Global:ArchitectureUsed = $Architecture
                    
                    # if ($architecture -eq 'x64') {
                    # $architectureFilter = '\(64-bit\)'
                    # }
                    # else {
                    # $architectureFilter = 'Offline'
                    # }
            
                    # $URL = "https://www.java.com/en/download/manual.jsp"
                    # $global:ie = New-Object -com "InternetExplorer.Application"
                    # $global:ie.visible = $false
                    # $global:ie.Navigate($URL)
            
                    # DO { Start-Sleep -s 1 }UNTIL(!($global:ie.Busy))
                    # Start-Sleep 5
                    # if ($global:ie.Document.body.innerHTML) {
                    # $HTML = $global:ie.Document.body.innerHTML.ToString()
                    # }
                    # else { return $null }
                    
                    # $Pattern = '<a href=\"(?<link>.*)\" title=\"Download Java software for Windows {0}\">Windows Offline' -f $architectureFilter
                    # $AllMatches = ([regex]$Pattern).Matches($HTML)
                    # $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://javadl.oracle.com/webapps/download/AutoDL?BundleId=249203_b291ca3e0c8548b5a51d5a5f50063037'
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://javadl.oracle.com/webapps/download/AutoDL?BundleId=249201_b291ca3e0c8548b5a51d5a5f50063037'
                            Architecture = 'x86'
                        }
                    )
    
                    break
                }
                'LandOnline Print-to-tiff Driver' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://rguiamoy-public.s3.ap-southeast-1.amazonaws.com/Setup_x64_LandOnlinePrintToTiff_v3.03.zip'
                    }

                    break
                }
                'LEAP' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.dropbox.com/scl/fi/w7qdxme2df2s1ad9myk5t/LEAPDesktopX64Setup.exe?rlkey=11sins6zi694kgp9kapzb5ztq&dl=1'
                    }

                    break
                }
                'LegalAid Templates' {
                    $domainName = 'https://www.justice.govt.nz'
                    $HTML = Invoke-RestMethod "$domainName/about/lawyers-and-service-providers/legal-aid-lawyers/forms/download-word-template-package/"
                    $Pattern = '<a title=\"LegalAid Templates Version \d+ installer\" href=\"(?<link>.*)\">Word'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = $domainName + ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'LOLComponents' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class="button" href=\"(?<link>.*)\">Landonline Client Components \(ZIP .+?\)</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Microsoft 365' {
                    $HTML = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
                    $Pattern = '<td class=\"file-link\"><a href=\"(?<link>.*)\"><span'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Mozilla Firefox' {

                    $downloadLinkFormat = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win{0}&lang=en-US'
                    $x64DownloadLink = $downloadLinkFormat -f '64'
                    $x86DownloadLink = $downloadLinkFormat -f ''

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = (Invoke-WebRequest -Uri $x64DownloadLink -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = (Invoke-WebRequest -Uri $x86DownloadLink -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Net Monitor for Employees Agent' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://networklookout.com/dwn/nmemplpro_agent.msi'
                    }

                    break
                }
                'Nitro PDF Pro' {
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber
                    $architecture = Resolve-ArchitectureSelection -ProgramName $ProgramName -Architecture $Architecture
                    $majorVersion = ($latestVersion.Split("."))[0]

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://downloads.gonitro.com/professional_{0}/en/nls/nitro_pro{1}_{2}.msi' -f $latestVersion, $majorVersion, $architecture
                    }
                    
                    break
                }
                'Notepad++' {
                    
                    $NPlusPlusWebsite = "https://notepad-plus-plus.org"
                    $DownloadPage = $NPlusPlusWebsite + "/downloads"

                    $currentVersion = $($(Invoke-WebRequest -Uri $DownloadPage).Links | Where-Object innerText -like 'Current Version*').href

                    if (!$currentVersion) {
                        Write-Log -LogType ERROR -Message "Unable to find current version."
                        return $null
                    }

                    $NPlusPlusCurrentVersionDownloadPage = $NPlusPlusWebsite + $currentVersion

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $(Invoke-WebRequest $NPlusPlusCurrentVersionDownloadPage).Links.href -like '*Installer.x64.exe' | Select-Object -First 1
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $(Invoke-WebRequest $NPlusPlusCurrentVersionDownloadPage).Links.href -like '*Installer.exe' | Select-Object -First 1
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'NZCS IT Support' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://ezpsa.com/downloads/ITSupportApp/NZCSSupportSetup.exe'
                    }

                    break
                }
                { $_ -in "Patriot Task Service", "Patriot Version 6 Client", "Patriot Reporting Components" } {
                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://ezpsa.com/downloads/patriot/UpdPat611182Stable.zip'
                    }
        
                    break
                }
                'Patriot VLC Extension' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.patriotsystems.com/downloads/Patriot_VLC_Extension_3.5.1.0.exe'
                    }

                    break
                }
                'PDFCreator' {
                    $originalLink = 'https://download.pdfforge.org/download/pdfcreator/PDFCreator-stable?download'

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Philips Device Connector' {
                    $originalLink = 'https://www.dictation.philips.com/pcl8000/nativehostinstaller_win/'

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.dictation.philips.com' + (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'Putty' {
                    $PuttyDownloadPage = "https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html"
            
                    try {
                        $links = (Invoke-WebRequest $PuttyDownloadPage).Links.href 
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
            
                    if (!$links) {
                        Write-Log -LogType ERROR -Message "No links found."
                        return $null                    
                    }
                   
                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $links | Where-Object { ($_ -match "w64/(.+?)-installer\.msi$") }
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $links | Where-Object { ($_ -match "w32/(.+?)-installer\.msi$") }
                            Architecture = 'x86'
                        }
                    )
            
                    break
                }
                'Python 3' {

                    $x64DownloadLink = $(Invoke-WebRequest -Uri 'https://www.python.org/downloads/').Links.href | Where-Object { $_ -like '*.exe' }

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $x64DownloadLink
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $x64DownloadLink -replace "-amd64", ""
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Remote Access Tool' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Remote Access Installer.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)

                    $downloadLink = [PSCustomObject]@{
                        URL = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    }

                    break
                }
                'Snagit' {
                    $latestVersionID = (Invoke-RestMethod 'https://www.techsmith.com/api/v/1/products/getallversions/12')[0].VersionID
                    $downloadInformation = (Invoke-RestMethod "https://www.techsmith.com/api/v/1/products/getversioninfo/$latestVersionID").PrimaryDownloadInformation

                    $downloadLink = [PSCustomObject]@{
                        URL = "https://download.techsmith.com" + $downloadInformation.RelativePath + "snagit.msi"
                    }

                    break
                }
                'ServiceCATRMM' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://ezpsa.com/downloads/scaudit/ServiceCatSetup.exe'
                    }

                    break
                }
                'Synology Drive Client' {
                    $downloadLinkFormat = 'https://global.download.synology.com/download/Utility/SynologyDriveClient/{0}/Windows/Installer/i686/Synology%20Drive%20Client-{0}-x86.msi?model=DS220'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'

                    $downloadLink = [PSCustomObject]@{
                        URL = $downloadLinkFormat -f $latestVersion
                    }

                    break
                }
                'Synology Active Backup for Business Agent' {
                    
                    $downloadLinkFormat = 'https://global.synologydownload.com/download/Utility/ActiveBackupBusinessAgent/{0}/Windows/{1}/Synology%20Active%20Backup%20for%20Business%20Agent-{0}-{2}.msi?model=DS220'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $downloadLinkFormat -f $latestVersion, 'x86_64', 'x64'
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $downloadLinkFormat -f $latestVersion, 'i686', 'x86'
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Sysmon64' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://download.sysinternals.com/files/Sysmon.zip'
                    }

                    break
                }
                'Microsoft Teams' {
                    $originalLink = 'https://teams.microsoft.com/downloads/desktopcontextualinstaller?env=prod&intent=work&plat=windows&download=true'

                    $downloadLink = [PSCustomObject]@{
                        URL = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    }

                    break
                }
                'TreeSize Free' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://downloads.jam-software.de/treesize_free/TreeSizeFreeSetup.exe'
                    }

                    break
                }
                'Trend Micro Security Agent' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://ezpsa.com/downloads/files/WFBS-SVC_Agent_Installer.msi'
                    }

                    break
                }
                'UniPrint' {
                    
                    $regex = "UniPrintClientMSI_\d+_{0}.zip$"
                    $webRequest = (Invoke-WebRequest -Uri 'https://www.uniprint.net/en/uniprint-client/' -UseBasicParsing).Links.href 

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = $webRequest | Where-Object { $_ -match $($regex -f 'x64') }
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = $webRequest | Where-Object { $_ -match $($regex -f 'x86') }
                            Architecture = 'x86'
                        }
                    )

                    break
                }                
                'UCS Client' {
                    
                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.ezpsa.com/downloads/aw/UCSClient.zip'
                    }

                    break
                }
                'VLC' {

                    $latestVersionNumber = (Get-LatestVersionNumber 'VLC').VersionNumber

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://get.videolan.org/vlc/{0}/win64/vlc-{0}-win64.exe' -f $latestVersionNumber
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://get.videolan.org/vlc/{0}/win32/vlc-{0}-win32.exe' -f $latestVersionNumber
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'Windirstat' {

                    Function Get-URLFromFosshub {
                        Param (
                            $Data
                        )

                        try { 
                            $Url = 'https://api.fosshub.com/download' 
                            $Params = @{ 
                                Uri             = $Url 
                                Body            = @{ 
                                    projectId  = "$($Data.projectId)" 
                                    releaseId  = "$($Data.pool.f.r | Select -Unique)" 
                                    projectUri = 'IrfanView.html' 
                                    fileName   = $((($Data).pool.f | Where-Object { $_.n -match ('windirstat1_1_2_setup\.exe') }))[0].n
                                    source     = "$($Data.pool.c)" 
                                }
                                Headers         = @{
                                    'User-Agent' = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                                }
                                Method          = 'POST'
                                UseBasicParsing = $true
                            }
                            
                            $info = (Invoke-WebRequest @Params).Content | ConvertFrom-Json
                            
                            $Global:ErrorType = $Response.error
                            if ($Global:ErrorType -ne $Null) {
                                throw "ERROR RETURNED $Global:ErrorType"
                                return $null
                            }

                            return ($info.data)[0].url
                        }
                        catch {
                            Write-Error $_
                        }                    
                    }

                    $webRequest = (Invoke-WebRequest -Uri "https://www.fosshub.com/WinDirStat.html" -UseBasicParsing).content 
                    #$data = ($test | Select-String -Pattern '(?<=\s=).*').matches.value | ConvertFrom-Json
                    $webRequest -match 'var settings =(.+?)\n' | Out-Null
                    $data = $matches[1] | ConvertFrom-Json

                    $downloadLink = [PSCustomObject]@{
                        URL = Get-URLFromFosshub -Data $data
                    }

                    break
                }
                'WinRAR' {

                    $webRequest = (Invoke-WebRequest https://www.rarlab.com/download.htm).Links
                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'https://www.rarlab.com' + ($webRequest | Where-Object href -match 'x64-\d+\.exe' | Select-Object href -First 1).href
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'https://www.rarlab.com' + ($webRequest | Where-Object href -match 'x32-\d+\.exe' | Select-Object href -First 1).href
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                'WinSCP' {
                    $downloadLinkFormat = 'https://downloads.sourceforge.net/project/winscp/WinSCP/{0}/WinSCP-{0}-Setup.exe'
                    $latestVersionNumber = (Get-LatestVersionNumber 'WinSCP').VersionNumber
                    $originalLink = $downloadLinkFormat -f $latestVersionNumber
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    $downloadLink = Get-RedirectedUrl -URL $downloadLink -Counter 2
                    

                    $downloadLink = [PSCustomObject]@{
                        URL = $downloadLink
                    }

                    break
                }
                'Wireshark' {

                    $wiresharkDownloadPage = "https://www.wireshark.org/download.html"
                    
                    $latestVersion = (Get-LatestVersionNumber -ProgramName Wireshark).VersionNumber
            
                    $webRequest = Invoke-WebRequest -Uri $WiresharkDownloadPage

                    $downloadLink = [PSCustomObject]@{
                        URL = $webRequest.Links.href | Where-Object { $_ -like "*-$latestVersion-x64.exe" }
                    }

                    break
                }
                'Yealink USB Connect' {

                    $downloadLink = [PSCustomObject]@{
                        URL = "https://www.yealink.com/" + ((Invoke-WebRequest 'https://www.yealink.com/en/product-detail/usb-connect-management' -UseBasicParsing).Links | Where-Object href -like '*yealink-usb-connect-*.msi').href
                    }

                    break
                }
                'ZelloWork' {

                    $downloadLink = [PSCustomObject]@{
                        URL = 'https://www.zello.com/data/mesh/ZelloWorkClient.msi'
                    }

                    break
                }
                'Zoom' {

                    $downloadLink = @(
                        [PSCustomObject]@{
                            URL          = 'http://zoom.us/client/latest/ZoomInstallerFull.msi?archType=x64'
                            Architecture = 'x64'
                        },
                        [PSCustomObject]@{
                            URL          = 'http://zoom.us/client/latest/ZoomInstallerFull.msi'
                            Architecture = 'x86'
                        }
                    )

                    break
                }
                Default {
                    Write-Log -LogType INFO -Message "No matching function to retrieve download link for $programName"
                    return $null
                }
            }
        }
        catch [System.NotSupportedException] {
            Disable-IEFirstRunCustomization
            continue
        }
        catch {
            Write-Log -LogType ERROR "Unable to retrieve download link for $ProgramName. $($Global:Error[0])"
        }

        if ($downloadLink) {
            return $downloadLink
        }

        $attempts++
        Start-Sleep 10
    }

    Write-Log -LogType ERROR -Message "The maximum number of attempts to retrieve the download link has been reached."
    return $null
}

Function Get-LatestVersionNumber {
    Param (
        $ProgramName
    )

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    try {
        switch ($ProgramName) {
            '7-zip' {            
                $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html' -ErrorAction Stop
                $Pattern = '<B>Download 7-Zip (?<version>[\d\.]+) \((.+?)\)</B>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Actionstep Office Add-In' {
                $downloadLink = Get-DownloadLink -ProgramName $ProgramName
                $downloadLink -match 'asoffice-(.+?)\.exe$' | Out-Null
                $latestVersion = $matches[1]
                break
            }
            'Adobe Acrobat' {
                # $versionRegex = '\d{2}\.\d{3}\.\w{5}'
                # $acrobatReleaseNotesURL = 'https://helpx.adobe.com/acrobat/release-note/release-notes-acrobat-reader.html'
                # $versionsAvailable = $(invoke-webrequest -uri $acrobatReleaseNotesURL -UseBasicParsing).Links | Where-Object { $_.outerHTML -match $versionRegex }
                # $latestVersionLink = $versionsAvailable[0].href
                        
                # $latestVersionHTML = Invoke-RestMethod $latestVersionLink -UseBasicParsing -Headers @{"accept" = "*/*" } -ErrorAction Stop
                # $Pattern = '\">AcrobatDCUpd(\d{2})(\d{3})(\d{5})\.msp</a>'
                # $latestVersionHTML -match $Pattern | Out-Null
                # $latestVersion = "$($matches[1]).$($matches[2]).$($matches[3])"
                
                $apiRequest = (Invoke-WebRequest 'https://rdc.adobe.io/reader/products?lang=en&site=otherversions&os=Windows%2011&&api_key=dc-get-adobereader-cdn').Content | ConvertFrom-Json
                $latestVersion = $apiRequest.products.reader[0].version
                break
            }
            'Adobe Digital Editions' {
                #$HTML = Invoke-RestMethod 'https://www.adobe.com/nz/solutions/ebook/digital-editions/download.html' -UseBasicParsing -Headers @{"accept" = "*/*" } -ErrorAction Stop
                $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                $HTML = Invoke-WebRequest -UseBasicParsing -Uri "https://www.adobe.com/solutions/ebook/digital-editions/download.html" -WebSession $session -Headers @{  "accept-encoding" = "gzip, deflate, br"; "accept-language" = "en-US,en;q=0.9,fil;q=0.8" }
                $Pattern = '<h2><b>Adobe Digital Editions (?<version>.*) Installers</b></h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Advanced IP Scanner' {
                $HTML = Invoke-RestMethod 'https://www.advanced-ip-scanner.com/download/'
                $Pattern = '<a href=\"(?<link>https://download\.advanced-ip-scanner\.com/download/files/Advanced_IP_Scanner_(?<version>.+?)\.exe)"' 
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Agent Ransack' {
                $downloadLink = (Get-DownloadLinkV2 'Agent Ransack').URL
                $downloadLink -match 'agentransack_(\d+).exe' | Out-Null
                $latestVersion = $matches[1]
                break
            }
            'Arcserve ShadowControl' {
                $HTML = Invoke-RestMethod 'https://www.arcserve.com/software-downloads/shadowprotect'
                $Pattern = "Endpoint version number: <span class='version'> (?<version>[\d\.]+) </span>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Arcserve ShadowProtect' {
                $HTML = Invoke-RestMethod 'https://www.arcserve.com/software-downloads/shadowprotect'
                $Pattern = "Arcserve ShadowProtect SPX \(Windows\) downloads</h3></div><div class='service-version'>Version number: <span class='version'> (?<version>[\d\.]+) </span>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Audacity' {
                $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/'  -ErrorAction Stop
                $Pattern = '<p>Current version (?<version>[\d\.]+)</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'BarTender' {
                $downloadLink = Get-DownloadLink -ProgramName 'BarTender'
                $regex = 'https://.+/Bartender/(.+?)/.+_R(.+?)_(.+?)_.+\.exe'
                $downloadLink -match $regex | Out-Null
                $latestVersion = "$($matches[1]).$($matches[2]).$($matches[3])"
                break
            }
            'Bitwarden' {
                $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows'
                $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                $Pattern = '/Bitwarden-Installer-(?<version>.*)\.exe'
                $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'CutePDF Writer' {
                $HTML = Invoke-RestMethod 'https://www.cutepdf.com/products/CutePDF/writer.asp' -ErrorAction Stop
                $Pattern = 'Ver\. (?<version>.*); .+? MB\)'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Citrix Workspace' {
                $HTML = Invoke-RestMethod 'https://www.citrix.com/downloads/workspace-app/workspace-app-for-windows-long-term-service-release/workspace-app-for-windows-LTSR-Latest.html' -ErrorAction Stop
                $Pattern = '<p><b>Version:</b>\&nbsp;(?<version>[\d\.]+)</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'DC Loader' {
                $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/digital-certificates-and-security/download-or-renew-your-two-year-digital-certificate'
                $Pattern = '<p><a class=\"button\" href=\".+?\">Download DC Loader.+? \(EXE .+?MB v(?<version>[\d\.]+) .+?\)</a></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Digisign' {
                $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads' -ErrorAction Stop
                # $Pattern = '<p><a class=\"button\" href=\".+?\">Digisign.+? \(EXE .+?MB v(?<version>[\d\.]+).+? .+?\)</a></p>'
                $Pattern = '<p><a class=\"button\" href=\".+?\">Digisign.+? \(EXE .+?MB v(?<version>\d+\.\d+).+? .+?\)</a></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Dropbox' {
                $HTML = Invoke-RestMethod 'https://www.dropboxforum.com/t5/forums/filteredbylabelpage/board-id/101003016/label-name/stable%20build' -ErrorAction Stop
                $Pattern = '<h3><a href=".*">Stable Build (?<version>.*)</a></h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft Edge' {
                $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/deployedge/microsoft-edge-relnote-stable-channel' -ErrorAction Stop
                $Pattern = '<h2 id=".*">Version (?<version>.*):.*</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'FileZilla' {
                $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
                $HTML = Invoke-RestMethod -UseBasicParsing -Uri "https://filezilla-project.org/download.php?show_all=1" -WebSession $session -ErrorAction Stop
                $Pattern = '<p>The latest stable version of FileZilla Client is (?<version>.*)</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Foxit PDF Reader' {
                $HTML = Invoke-RestMethod 'https://www.foxit.com/pdf-reader/version-history.html' -ErrorAction Stop
                $Pattern = '<h3>Version (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Github Desktop' {
                $downloadLink = Get-DownloadLinkV2 'Github Desktop'
                $downloadLink -match 'releases/(.+?)-' | Out-Null
                $latestVersion = $matches[1]

            }
            'GPL Ghostscript' {
                # $HTML = Invoke-RestMethod 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/latest' -ErrorAction Stop
                # $Pattern = '<h1 data-view-component="true" class="d-inline mr-3">Ghostscript/GhostPDL (?<version>.*)</h1>'
                # $AllMatches = ([regex]$Pattern).Matches($HTML)
                # $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                $latestVersion = '10.0.0'
                break
            }
            'Google Drive' {
                $HTML = Invoke-RestMethod 'https://support.google.com/a/answer/7577057?hl=en' -ErrorAction Stop
                $Pattern = '<p><em><strong>Windows( and macOS)?:</strong> Version (?<version>.*)</em></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Google Chrome' {
                $json = (Invoke-WebRequest -UseBasicParsing -Uri "https://versionhistory.googleapis.com/v1/chrome/platforms/win64/channels/stable/versions/all/releases?filter=endtime=none").Content | ConvertFrom-Json
                $latestVersion = $json.releases.version | Select-Object -Last 1
                break
            }
            'HP Support Assistant' {
                $HTML = Invoke-RestMethod 'https://support.hp.com/us-en/help/hp-support-assistant' -ErrorAction Stop
                $Pattern = '<span class="bannerVersion">Version <span class="ver">(?<version>.*)</span>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'IrfanView' {
                $HTML = Invoke-RestMethod 'https://www.irfanview.com/' -ErrorAction Stop
                $Pattern = '<h2>Get IrfanView \(<strong>version (?<version>.*)</strong>\)</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Jabra Direct' {
                $HTML = Invoke-RestMethod 'https://www.jabra.co.nz/Support/release-notes/release-note-jabra-direct' -ErrorAction Stop
                $Pattern = '<p><strong>Release version:</strong> (?<version>.*)<br>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Java 8' {
                $latestVersion = $null
                $maxAttempts = 5
            
                $attempts = 0
                while (($null -eq $latestVersion) -and ($attempts -lt $maxAttempts)) {
            
                    $URL = "https://www.java.com/en/download/manual.jsp"
                    $global:ie = New-Object -com "InternetExplorer.Application"
                    $global:ie.visible = $false
                    $global:ie.Navigate($URL)
            
                    DO { Start-Sleep -s 1 }UNTIL(!($global:ie.Busy))
                    Start-Sleep 5
                    $HTML = $global:ie.Document.body.innerHTML.ToString()
                    $Pattern = '<h4 class="sub">Recommended (?<version>.*)</h4>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
            
                    $attempts++
                }
                break
            }
            'LegalAid Templates' {
                $domainName = 'https://www.justice.govt.nz'
                $HTML = Invoke-RestMethod "$domainName/about/lawyers-and-service-providers/legal-aid-lawyers/forms/download-word-template-package/"
                $Pattern = '<a title=\"LegalAid Templates Version (?<version>[\d\.]+) installer\"'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365' {
                $UpdateChannel = 'Current Channel' 
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Current Channel' {
                $UpdateChannel = 'Current Channel' 
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Monthly Enterprise Channel' {
                $UpdateChannel = 'Monthly Enterprise Channel'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Semi-Annual Enterprise Channel (Preview)' {
                $UpdateChannel = 'Semi-Annual Enterprise Channel \(Preview\)'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Semi-Annual Enterprise Channel' {
                $UpdateChannel = 'Semi-Annual Enterprise Channel'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Mozilla Firefox' {
                # $Pattern = 'releases/(?<version>[\d\.]+)/'
                # $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                # $downloadLink = (Invoke-WebRequest -Uri $originalLink -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                # $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                # $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value

                $HTML = Invoke-RestMethod 'https://www.mozilla.org/en-US/firefox/releases/'
                $Pattern = '<a href=".*">(?<version>[\d\.]+)</a>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Net Monitor for Employees Agent' {
                $HTML = Invoke-RestMethod 'https://networklookout.com/' -ErrorAction Stop
                $Pattern = '<p>ver. (?<version>[\d\.]+)'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Nitro PDF Pro' {
                $HTML = Invoke-RestMethod 'https://www.gonitro.com/product-details/release-notes' -ErrorAction Stop
                $Pattern = '<h4>Latest version: (?<version>[\d\.]+)</h4>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Notepad++' {
                $HTML = Invoke-RestMethod 'https://notepad-plus-plus.org/downloads/' -ErrorAction Stop
                $Pattern = '<a href=".*"><strong>Current Version (?<version>.*)</strong></a>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'PDFCreator' {
                $HTML = (Invoke-WebRequest 'https://docs.pdfforge.org/pdfcreator/en/pdfcreator/introduction/whats-new/' -UseBasicParsing -ErrorAction Stop).Content
                $Pattern = '<h2>PDFCreator (?<version>.*)<a'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Philips Device Connector' {
                $HTML = Invoke-RestMethod 'https://www.dictation.philips.com/fileadmin/Products/lfh7445/ifu/extensions/en/changelog.html' -ErrorAction Stop
                $Pattern = '<p>Windows drivers: (?<version>[\d\.]+)</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Putty' {
                $HTML = Invoke-RestMethod 'https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html' -ErrorAction Stop
                $Pattern = '<TITLE>Download PuTTY: latest release \((?<version>.*)\)</TITLE>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Python 3' {
                $HTML = Invoke-RestMethod 'https://www.python.org/downloads/windows/' -ErrorAction Stop
                $Pattern = '<li><a href="/downloads/release/python-.+?/">Latest Python 3 Release - Python (?<version>.*)</a></li>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Snagit' {
                $latestVersionInfo = (Invoke-RestMethod 'https://www.techsmith.com/api/v/1/products/getallversions/12')[0]
                $latestVersion = "$($latestVersionInfo.Major).$($latestVersionInfo.Minor).$($latestVersionInfo.Maintenance)"
                break
            }
            'Synology Drive Client' {
                $HTML = Invoke-RestMethod 'https://www.synology.com/en-global/releaseNote/SynologyDriveClient' -ErrorAction Stop
                $Pattern = '<h3>Version: (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = (($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value) -replace '-', '.'
                break
            }
            'Synology Active Backup for Business Agent' {
                $HTML = Invoke-RestMethod 'https://www.synology.com/en-global/releaseNote/ActiveBackupBusinessAgent' -ErrorAction Stop
                $Pattern = '<h3>Version: (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = (($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value) -replace '-', '.'
                break
            }
            'Sysmon64' {
                $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon' -ErrorAction Stop
                $Pattern = '<h1 id="sysmon-.*">Sysmon v(?<version>.*)</h1>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft Teams' {
                # $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/officeupdates/teams-app-versioning' -ErrorAction Stop
                # $Pattern = '<h3 id="windows-public-cloud-version-history">Windows \(Public Cloud\) version history</h3>(\n.*){14}\n<td style="text-align: left;">(?<version>.*)</td>'
                # $AllMatches = ([regex]$Pattern).Matches($HTML)
                # $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                # break

                $downloadLink = Get-DownloadLink -ProgramName $ProgramName
                $downloadLink -match 'https://statics.teams.cdn.office.net/production-windows/(.+?)/' | Out-Null
                $latestVersion = $matches[1]
                break
            }
            'TreeSize Free' {
                $HTML = Invoke-RestMethod 'https://www.jam-software.com/treesize_free/changes.shtml' -ErrorAction Stop
                $Pattern = '<h3 class="collapsed-item__ttl">Version (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'UniPrint' {
                $HTML = Invoke-WebRequest 'https://www.uniprint.net/en/uniprint-client/' -ErrorAction Stop
                $Pattern = "<p>UniPrint Client .*; (?<version>.*) Autodetect and Install</p>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'VLC' {
                $HTML = Invoke-RestMethod 'https://www.videolan.org/vlc/download-windows.html' -ErrorAction Stop
                $Pattern = "<span id='downloadVersion'>\n\s*(?<version>[\d\.]+)</span>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Windirstat' {
                $HTML = (Invoke-WebRequest 'https://windirstat.net/download.html' -ErrorAction Stop).RawContent
                $Pattern = "Latest version: (?<version>[\d\.]+)"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'WinRAR' {
                $HTML = Invoke-RestMethod 'https://www.win-rar.com/whatsnew.html?&L=0' -ErrorAction Stop
                $Pattern = '<h2>Version (?<version>[\d\.]+)</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'WinSCP' {
                $HTML = Invoke-RestMethod 'https://winscp.net/eng/download.php'
                $Pattern = '<a .+?>Download <strong>WinSCP</strong> (?<version>[\d\.]+) .+?</a>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Wireshark' {
                $HTML = Invoke-RestMethod 'https://www.wireshark.org/download.html' -ErrorAction Stop
                $Pattern = "<summary>Stable Release: (?<version>[\d\.]+) </summary>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Yealink USB Connect' {

                # temporary solution until a better way is available

                $downloadLink = Get-DownloadLinkV2 'Yealink USB Connect'
                # $download = Invoke-WebRequest -URI $downloadLink.URL
                # $content = [System.Net.Mime.ContentDisposition]::new($download.Headers["Content-Disposition"])
                # $installerFileName = $content.FileName

                $downloadLink.URL -match 'yealink-usb-connect-(.+?)\.msi' | Out-Null
                $latestVersion = $matches[1]
                
                # $downloadLink -match 'yealink-usb-connect-(.+?)\.msi' | Out-Null
                # $latestVersion = $matches[1]
                break
            }
            'Zoom' {
                $Pattern = 'prod/(?<version>.*)/ZoomInstallerFull\.msi'
                $originalLink = 'https://zoom.us/client/latest/ZoomInstallerFull.msi'
                $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value

                # We remove the third number in the retrieved version as it is not placed in the display version of zoom when installed.
                $splitVersion = $latestVersion -split '\.'
                $latestVersion = ($splitVersion[0..1] + $splitVersion[3..($splitVersion.Length - 1)]) -join '.'
                break
            }
            default {
                Write-Log -LogType INFO "No available method to retrieve latest version number available for $ProgramName."

                $latestVersion = 'NO METHOD AVAILABLE'
            }
        }
    
    }
    catch [System.NotSupportedException] {
        Disable-IEFirstRunCustomization
    }
    catch {
        Write-Log -LogType ERROR "Unable to retrieve latest version number for $ProgramName. $($Global:Error[0])"
        return $null
    }

    $obj = [PSCustomObject]@{
        ProgramName   = $ProgramName
        VersionNumber = $latestVersion
    }

    return $obj

}

Function Confirm-InstallerDigitalSignature {
    Param(
        $InstallerFilePath
    )

    $varChain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain
    try {
        $verification = $varChain.Build((Get-AuthenticodeSignature -FilePath "$InstallerFilePath").SignerCertificate)
    }
    catch [System.Management.Automation.MethodInvocationException] {
        $err = ( "'$InstallerFilePath' did not contain a valid digital certificate. " +
            "Something may have corrupted/modified the file during the download process. " +
            "Suggest trying again, contact support@appmani.com if it fails >2 times")
        Write-Log -LogType ERROR -Message $err
        return $null
    }
    catch {
        Write-Warning "The script ran into an issue: $($Global:Error[0])"
    }

    if (!$verification) {
        $err = ( "'$InstallerFilePath' did not contain a valid digital certificate. " +
            "Something may have corrupted/modified the file during the download process. " +
            "Suggest trying again, contact support@appmani.com if it fails >2 times")
        Write-Log -LogType ERROR -Message $err
    }

    return $verification
}

Function Confirm-InstallerHash {
    Param (
        $InstallerFilePath,
        $SourceHash
    )

    if (!$SourceHash) {
        Write-Log -LogType ERROR "Missing hash from installer meta data."
        return $false
    }

    Write-Log -LogType INFO "$InstallerFilePath"

    $downloadedInstallerHash = $(Get-FileHash -Algorithm SHA256 $InstallerFilePath).Hash
    if (!$downloadedInstallerHash) {
        Write-Log -LogType ERROR "Unable to retrieve downloaded installer's hash."
        return $false
    }

    Write-Log -LogType DEBUG "Reference file SHA256: $SourceHash"
    Write-Log -LogType DEBUG "Local file SHA256: $downloadedInstallerHash"

    if ($SourceHash -ne $downloadedInstallerHash) {
        Write-Log -LogType ERROR "Hash from installer metadata does not match downloaded installer's hash. Aborting installation."
        return $false
    }

    return $true

}

Function Set-AgentRefresh {
    Param (
        $NewRefreshCheckValue
    )

    $auditRefreshRegistryPath = 'HKLM:\SOFTWARE\NZCS\ServiceCAT'
    $auditRefreshRegistryItemName = 'RefreshCheck'

    $RegistryValueObj += [PSCustomObject]@{ValueName = $auditRefreshRegistryItemName; ValueData = $NewRefreshCheckValue; ValueType = 'DWORD' }

    # Gets refresh check value
    $CurrentRefreshCheckValue = (Read-RegistryValueData -RegistryKey $auditRefreshRegistryPath -ValueName $auditRefreshRegistryItemName).Value

    # Checks if new and current RefreshCheck values are different
    if ($NewRefreshCheckValue -ne $CurrentRefreshCheckValue) {
        # Sets new RefreshCheck value if they are different
        Add-RegistryValue -RegistryKey $auditRefreshRegistryPath -RegistryValueObj $RegistryValueObj | Out-Null
    
    }
}

Function Confirm-LogFolder {

    $parentFolderPath = 'C:\Windows\Temp\AppManiProgramManagerLogs\'
    $logFolderPath = $parentFolderPath + $global:scriptName

    $logFolder = Test-Path $logFolderPath

    if (!($logFolder)) {
        try {
            New-Item -Path $logFolderPath -ItemType Directory | Out-Null            
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to create log folder: $($Global:Error[0])"
            return $false
        }
    }

    Write-Log -LogType DEBUG -Message "Logs will be saved at $logFolderPath."
    return $true
}

Function Write-Log {
    Param (
        $LogType,
        $Message,
        $FGColor
    )

    $parentFolderPath = 'C:\Windows\Temp\AppManiProgramManagerLogs\'
    $logFolderPath = $parentFolderPath + $global:scriptName
    $logFileName = "$global:scriptName-" + (Get-Date -Format 'yyMMdd') + '.log'
    $logFilePath = "$logFolderPath\" + $logFileName

    $longDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    $msg = '{0} {1} {2}: {3}' -f $longDate, $global:scriptName, $LogType, $Message
    Add-Content -Path $logFilePath -Value $msg -ErrorAction Ignore
    
    if (($LogType -eq 'DEBUG') -and ($global:debug -eq 'true')) {

        Write-Host $msg -ForegroundColor 'Cyan'

        return
    }
    elseif (($LogType -eq 'WARNING') -or ($LogType -eq 'ERROR')) {
        switch ($LogType) {
            'WARNING' { $bgColor = 'Yellow' }
            'ERROR' { $bgColor = 'Red' }
        }

        Write-Host $msg -ForegroundColor $bgColor

        return
    }
    elseif ($LogType -eq 'INFO') {
        Write-Host $msg
    }

}

Function Disable-IEFirstRunCustomization {
    Write-Log -LogType INFO -Message "Unable to execute WebRequest. Disabling IE First RunCustomization..."
    try {
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main" -Name "DisableFirstRunCustomize" -Value 2
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to disable IE First RunCustomization: $($Global:Error[0])"
        return $null
    }
}


Function Get-GUID($maxSize = 10) {
    $g = [guid]::NewGuid()
    $v = [string]$g
    $v = $v.Replace("-", "")
    return $v.substring(0, $maxSize)
}

Function Set-Alert {
    Param (
        $AlertsFolderPath,
        [int]$Category = 1999,
        $Subject,
        $Body,
        [int]$Priority = 3,
        $AutoComplete,
        $JsonDepth = 2
    )

    $newAlert = [PSCustomObject]@{
        Category     = $Category
        Subject      = $Subject
        Body         = $Body
        Priority     = $Priority
        AutoComplete = $AutoComplete
    }

    if (!(Test-Path $AlertsFolderPath)) {
        try {
            New-Item -Path $AlertsFolderPath -ItemType Directory | Out-Null
            Write-Log -LogType INFO -Message "Alert folder created."
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to create alerts folder: $($Global:Error[0])"
            return $null
        }
    }

    $newAlertFilePath = $AlertsFolderPath + '\alert_' + $(Get-GUID) + '.json'

    try {
        $newAlert | ConvertTo-Json -Depth $JsonDepth | Out-File $newAlertFilePath
        Write-Log -LogType INFO -Message "Alert generated at $newAlertFilePath."
    }
    catch {
        Write-Log -LogType ERROR -Mesage "Unable to write alert to alerts.json: $($Global:Error[0].Exception)"
    }
}

Function Add-UploadDataFile {
    Param (
        $FilesFolderPath = "C:\ProgramData\AppMani\ServiceCATRMM\work\files",
        $FilePath
    )

    if (!(Test-Path -Path $FilePath -PathType Leaf)) {
        Write-Log -LogType ERROR -Message "Path is not a file or does not exist."
        return $null
    }

    if (!(Test-Path $FilesFolderPath)) {
        try {
            New-Item -Path $FilesFolderPath -ItemType Directory | Out-Null
            Write-Log -LogType INFO -Message "File folder created."
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to create file folder: $($Global:Error[0])"
            return $null
        }
    }

    try {
        $directoryPath = Split-Path -Parent $filePath
        $fileName = Split-Path -Leaf $filePath
        $fileExtension = [System.IO.Path]::GetExtension($filePath)
        $fileSize = (Get-Item $filePath).length
    }
    catch {
        Write-Log -LogType ERROR -Message "An error occurred: $($Global:Error[0])"
    }

   
    # Add-Type -AssemblyName "System.Web"
    # $mimeType = [System.Web.MimeMapping]::GetMimeMapping($FilePath)

    $extensionWhitelist = @('.log', '.txt', '.gif', '.png', '.jpg') # Define your whitelist extensions here
    $maxSize = 20MB # 20MB limit
    $maxFileNameChars = 100

    # Only allow files with extensions that belongs to the whitelist
    if ($extensionWhitelist -contains $fileExtension) {
        Write-Log -LogType DEBUG -Message "File extension $fileExtension is in the whitelist."
    }
    else {
        Write-Log -LogType ERROR -Message "File extension $fileExtension is not in the whitelist."
        return $null
    }

    # Check if filename contains any invalid characters
    if ($filename.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ne -1) {
        # The filename contains invalid characters
        Write-Log -LogType ERROR -Message "Filename contains invalid characters."
    }
    else {
        # The filename does not contain invalid characters
        Write-Log -LogType DEBUG -Message "Filename is valid."
    }

    # Only allow files that are below 20MB
    if ($fileSize -le $maxSize) {
        Write-Log -LogType DEBUG -Message "File size $("{0:N2}" -f ($fileSize / 1MB))MB is within the $("{0:N2}" -f ($maxSize / 1MB))MB limit."
    }
    else {
        Write-Log -LogType ERROR -Message "File size $("{0:N2}" -f ($fileSize / 1MB))MB exceeds the $("{0:N2}" -f ($maxSize / 1MB))MB limit."
        return $null
    }

    # Only allow files that have less than or equal to 100 characters
    if ($fileName.Length -le $maxFileNameChars) {
        Write-Log -LogType DEBUG -Message "Filename is within the $maxFileNameChars-character limit."
    }
    else {
        Write-Log -LogType ERROR -Message "Filename exceeds the $maxFileNameChars-character limit."
        return $null
    }

    $uploadDataFile = [PSCustomObject]@{
        Path          = $directoryPath
        FileName      = $fileName
        FileExtension = $fileExtension
        MimeType      = $mimeType
    }

    $newUploadFilePath = $FilesFolderPath + '\file_' + $(Get-GUID) + '.json'

    try {
        $uploadDataFile | ConvertTo-Json | Out-File $newUploadFilePath
        Write-Log -LogType INFO -Message " Upload data file generated at $newUploadFilePath."
    }
    catch {
        Write-Log -LogType ERROR -Mesage "Unable to write upload data file: $($Global:Error[0].Exception)"
    }
}

Function Get-UninstallCommand {
    Param (
        [Parameter(ValueFromPipeline = $true)]    
        $InstalledProgram,
        [Switch]$UseStringsExe
    )
    
    # This function adds double-quotes to the uninstall executable path so paths with spaces won't error when invoked
    Function Add-QuotesToPath {
        Param (
            $Command
        )

        $matches = @()
        $path = Find-Path -String $Command
        $quoteCheckRegex = '\"{0}\"' -f [regex]::Escape($path)
        if (!($Command -match $quoteCheckRegex)) {
            $Command = $Command.replace($path, "`"$path`"")
            return $Command
        }
        return $Command
    }
    
    # Checks if UninstallString property is present
    $uninstallCommand = $InstalledProgram.UninstallString
    if (!($uninstallCommand)) {
        Write-Log -LogType ERROR "Uninstall string missing from registry."
        return $null
    }

    # Uninstall string overrides and customizations'
    switch -Wildcard ($InstalledProgram.DisplayName) {
        'CutePDF Writer*' {
            Write-Log -LogType DEBUG -Message "Override found."

            # The uninstall command doesn't have quotes on the uninstall exe path which has a space on it so it will fail when called from cmd (C:\Program Files (x86)\CutePDF Writer\unInstcpw64.exe /uninstall)
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' /s'
            return $uninstallCommand
        }
        'Digisign*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $App = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Digisign*" | Where-Object DisplayName -eq $InstalledProgram.DisplayName
            $uninstallCommand = $App.UninstallString
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' /qn'
            return $uninstallCommand
        }
        'Citrix Workspace*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $uninstallCommand += ' /silent'
            return $uninstallCommand
        }
        # 'Dropbox*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand

        # }
        # 'FileZilla*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        'Google Drive*' {
            Write-Log -LogType DEBUG -Message "Override found."

            # The uninstall command doesn't have quotes on the uninstall exe path which has a space on it so it will fail when called from cmd (C:\Program Files (x86)\CutePDF Writer\unInstcpw64.exe /uninstall)
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' --silent --force_stop'
            return $uninstallCommand
        }
        # 'GPL Ghostscript*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        'IrfanView*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $uninstallCommand += ' /silent'
            return $uninstallCommand
        }
        'Jabra Direct*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $App = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object DisplayName -eq $($InstalledProgram.DisplayName)
            $uninstallCommand = $App.QuietUninstallString
            return $uninstallCommand
        }
        'UCS Client*' {

            Write-Log -LogType DEBUG -Message "Override found."

            $uninstallcommand = (Get-InstalledProgram 'UCS Client' -All | Where-Object UninstallString -like '*setup.exe*').UninstallString
            $uninstallFolder = Split-Path -Path (Find-Path -String $uninstallCommand)
            $uninstallSetupIss = @"
[{57453723-99EC-478B-9D64-8A126FF638A0}-DlgOrder]
Dlg0={57453723-99EC-478B-9D64-8A126FF638A0}-MessageBox-0
Count=2
Dlg1={57453723-99EC-478B-9D64-8A126FF638A0}-SdFinish-0
Dlg2={57453723-99EC-478B-9D64-8A126FF638A0}-SdCustomerInfo-0
Dlg3={57453723-99EC-478B-9D64-8A126FF638A0}-SdAskDestPath-0
Dlg4={57453723-99EC-478B-9D64-8A126FF638A0}-SdStartCopy-0
Dlg5={57453723-99EC-478B-9D64-8A126FF638A0}-SdFinish-0
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdWelcome-0]
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdLicense2Rtf-0]
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdCustomerInfo-0]
szName=test
szCompany=test
nvUser=1
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdAskDestPath-0]
szDir=C:\Program Files (x86)\UCS Client\
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdStartCopy-0]
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdFinish-0]
Result=1
bOpt1=1
bOpt2=0
[{57453723-99EC-478B-9D64-8A126FF638A0}-SprintfBox-0]
Result=6
[{57453723-99EC-478B-9D64-8A126FF638A0}-SdSetupCompleteError-12060]
Result=1
[{57453723-99EC-478B-9D64-8A126FF638A0}-MessageBox-0]
Result=6
"@
 
            $uninstallSetupIss | Set-Content "$uninstallFolder\setup.iss"
            $uninstallCommand += ' -s'
            return $uninstallCommand
        }
        # 'Mozilla Firefox*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        # 'Python 3*' {
        # Write-Log -LogType INFO -Message "Override found."
        # $installerLocationFolder = 'C:\Windows\Temp\Python3\'
        # $installerFilename = (Split-Path $installedProgram.BundleCachePath -leaf)
        # $installerLocation = $installerLocationFolder + $installerFileName

        # #if (Test-Path '')
        # }
        # 'TreeSize Free*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /VERYSILENT /NORESTART'
        # return $uninstallCommand
        # }
        # 'VLC*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        'TreeSize Free*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $uninstallCommand += ' /VERYSILENT'
            return $uninstallCommand
        }

        'WinRAR*' {
            Write-Log -LogType DEBUG -Message "Override found."

            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' /S'
            return $uninstallCommand
        }

        Default {
            Write-Log -LogType DEBUG -Message "No override found."
        }
    }

    # If QuietUninstallString property is present, return right away
    if ($InstalledProgram.QuietUninstallString) {
        Write-Log -LogType DEBUG -Message "Quiet uninstall command found from the registry."
        return $InstalledProgram.QuietUninstallString
    }


    # If UninstallString uses MsiExec, make sure we are using /X and not /I and add /QN at the end
    if ($uninstallCommand -like 'MsiExec*') {
        Write-Log -LogType DEBUG -Message "Msiexec uninstall command found from the registry."
        $msiExecRegex = '{(.+?)}'
        if (!($uninstallCommand -match $msiExecRegex)) { 
            Write-Log -LogType ERROR -Message "Unable to extract product GUID."
            return $null 
        }
        else { $GUID = $matches[1] }

        $uninstallCommand = "MsiExec.exe /X{$GUID} /qn"
        return $uninstallCommand
    }

    #Write-Log -LogType ERROR "No silent install command has been configured for $ProgramName."
    Write-Log -LogType DEBUG "Attempting to determine quiet uninstall command for $($InstalledProgram.DisplayName)."
    $uninstallExePath = Find-Path -String $uninstallCommand

    if ($UseStringsExe) {
        Write-Log -LogType DEBUG "Retrieving via strings.exe."
        $stringsPath = 'C:\Windows\TEMP\strings.exe'
        if (!(Test-Path $stringsPath)) {
            Write-Log -LogType ERROR -Message "Unable to locate $stringsPath."
            return $null
        }
        
        $installerTool = C:\Windows\TEMP\strings.exe $uninstallExePath /accepteula | Select-String -Pattern @("InstallAware", "Inno Setup", "InstallShield", "Nullsoft", "Advanced Installer") | Select -First 1
    }
    else {

        Write-Log -LogType DEBUG "Retrieving via native functions."

        # Does what strings.exe does
        $str = ""
        $binary = [System.IO.File]::ReadAllBytes($uninstallExePath)
        $encoding = [System.Text.Encoding]::GetEncoding("Windows-1252")
        $stringBuilder = New-Object System.Text.StringBuilder  
        for ($i = 0; $i -lt $binary.Length; $i++) {
            # Check if the current byte is a valid ANSI character
            $chars = $encoding.GetChars($binary, $i, 1)
            if ([regex]::IsMatch($chars, "[\x20-\x7E]+")) {
                $stringBuilder.Append($chars) | Out-Null
            }
            elseif ($stringBuilder.Length -gt 0) {
                $str += $stringBuilder.ToString()
                $stringBuilder.Clear() | Out-Null
            }
        } 

        $installerTool = $str | Select-String -Pattern @("InstallAware", "Inno Setup", "InstallShield", "Nullsoft", "Advanced Installer") | Select-Object -First 1

    }

    if (!$installerTool) {
        Write-Log -LogType DEBUG -Message "Unable to determine installer tool."
    }

    $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
    switch -Wildcard ($installerTool) {
        '*InstallAware*' {
            Write-Log -LogType DEBUG -Message "Installer created using InstallAware."
            $uninstallCommand += ' /s'
            return $uninstallCommand
        }
        '*Inno Setup*' {
            Write-Log -LogType DEBUG -Message "Installer created using Inno Setup."
            $uninstallCommand += ' /VERYSILENT /NORESTART'
            return $uninstallCommand
        }
        '*InstallShield*' {
            Write-Log -LogType DEBUG -Message "Installer created using InstallShield."
            $uninstallCommand += ' -s'
            return $uninstallCommand
        }
        '*Nullsoft*' {
            Write-Log -LogType DEBUG -Message "Installer created using Nullsoft."
            $uninstallCommand += ' /S'
            return $uninstallCommand
        }
        '*Advanced Installer*' {
            Write-Log -LogType DEBUG -Message "Installer created using Advanced Installer."
            $uninstallCommand += ' /quiet'
            return $uninstallCommand
        }
    }

    Write-Log -LogType INFO -Message "Unable to determine quiet uninstall command. Please contact an Administrator."
    return $null

}

Function Uninstall-Program {
    Param (
        $uninstallCommand
    )

    Write-Log -LogType DEBUG -Message "Executing command $uninstallCommand"

    try {
        cmd /c $uninstallCommand
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to uninstall: $($Global:Error[0])"
        return 1
    }

    Write-Log -LogType INFO -Message "Execution completed with exit code $LASTEXITCODE"
    return $LASTEXITCODE
}

Function Get-ProgramRegistryDisplayRegex {
    Param (
        $ProgramName
    )

    $RegistryDisplayNameRegexes = @{
        'Citrix Workspace' = '^Citrix Workspace \d+$'
        'Dropbox'          = '^Dropbox$'
        'Microsoft Edge'   = '^Microsoft Edge$'
        'Notepad++'        = 'Notepad\+\+'
        'Python 3'         = 'Python 3\.[\d\.]+ \(.+\)'
    }

    $regex = $RegistryDisplayNameRegexes.$ProgramName
    if (!$regex) { return $ProgramName }
    else { return $regex }
}


Function Get-InstallCommand {
    Param (
        $ProgramName
    )

    $InstallCommands = @{
        '7-Zip'                                     = '"{0}" /S'
        'Actionstep Office Add-In'                  = '"{0}" /s /v"/qn"'
        'Adobe Acrobat'                             = '"{0}" /sAll /rs /rps /msi /norestart /quiet EULA_ACCEPT=YES'
        'Adobe Digital Editions'                    = '"{0}" /S'
        'Advanced IP Scanner'                       = '"{0}" /VERYSILENT /NORESTART'
        'Agent Ransack'                             = '"{0}" /S /uin'
        'Ajax PRO Desktop'                          = '"{0}" /VERYSILENT /NORESTART'
        'Arcserve ShadowControl'                    = 'msiexec.exe /i "{0}" /qn /norestart'
        'Arcserve ShadowProtect'                    = 'msiexec.exe /i "{0}" /qn /norestart /Lime "{1}" IACCEPT=STORAGECRAFT.EULA KEY="{2}" NAME="{3}" ORG="{4}"'
        'Audacity'                                  = '"{0}" /VERYSILENT /NORESTART'
        'BitWarden'                                 = '"{0}" /allusers /S'
        'Citrix Workspace'                          = '"{0}" /silent /noreboot'
        'CutePDF Writer'                            = '"{0}" /VERYSILENT /NORESTART'
        'DC Loader'                                 = '"{0}" /quiet'
        'Digisign'                                  = '"{0}" /quiet'
        'Dropbox'                                   = '"{0}" /NOLAUNCH'
        'Filezilla'                                 = '"{0}" /S'
        'Foxit PDF Reader'                          = '"{0}" /VERYSILENT /NORESTART'
        'Github Desktop'                            = '"{0}" -s'
        'Google Chrome'                             = 'msiexec.exe /i "{0}" /qn /norestart'
        'Google Drive'                              = '"{0}" --silent --desktop_shortcut'
        'GPL Ghostscript'                           = '"{0}" /S'
        'HP Support Assistant'                      = '"{0}" /S /v/qn'
        'IrfanView'                                 = '"{0}" /silent /group=1 /allusers=1'
        'ISQLME'                                    = '"{0}" /Silent'
        'Jabra Direct'                              = '"{0}" /install /quiet /norestart'
        'Java 8'                                    = '"{0}" /s REBOOT=0 SPONSORS=0 AUTO_UPDATE=0'
        'LandOnline Print-to-tiff Driver'           = 'msiexec.exe /i "{0}" /qn'
        'LEAP'                                      = '"{0}" /S /v/qn'
        'LegalAid Templates'                        = 'msiexec /q /i "{0}"'
        'LOLComponents'                             = 'msiexec /q /i "{0}"'
        'Microsoft Edge'                            = '"{0}" /silent /install'
        'Microsoft Teams'                           = '"{0}" -s'
        'Microsoft 365'                             = '"{0}" /configure "{1}\{2}"'
        'Mozilla Firefox'                           = '"{0}" -ms -ma'
        'Nitro PDF Pro'                             = 'msiexec.exe /i "{0}" /qn /norestart'
        'Notepad++'                                 = '"{0}" /S'
        'NZCS IT Support'                           = '"{0}" /Silent'
        'Patriot Task Service'                      = 'msiexec /i "{0}" TARGETDIR="C:\Program Files (x86)\" /qn /norestart'
        'Patriot Version 6 Client'                  = 'msiexec /i "{0}" TARGETDIR="C:\Program Files (x86)\" /qn /norestart'
        'Patriot VLC Extension'                     = '"{0}" /S'
        'Patriot Reporting Components'              = 'msiexec /i "{0}" TARGETDIR="C:\Program Files (x86)\" /qn /norestart'
        # 'Patriot Task Service' = 'msiexec /a "{0}" TARGETDIR="C:\Program Files (x86)\" /QN'
        # 'Patriot Version 6 Client' = 'msiexec /a "{0}" TARGETDIR="C:\Program Files (x86)\" /QN'
        # 'Patriot Reporting Components' = 'msiexec /a "{0}" TARGETDIR="C:\Program Files (x86)\" /QN'
        'PDFCreator'                                = '"{0}" /VERYSILENT /NORESTART'
        'Philips Device Connector'                  = '"{0}" /s /v"/qn"'
        'Putty'                                     = 'msiexec.exe /i "{0}" /qn'
        'Python 3'                                  = '"{0}" /quiet InstallAllUsers=1 PrependPath=1'
        'Remote Access Tool'                        = 'msiexec /q /i "{0}"'
        'Snagit'                                    = 'msiexec.exe /i "{0}" /qn /norestart'
        'ServiceCATRMM'                             = '"{0}" /silent'
        'Sophos Endpoint Agent'                     = '"{0}" --products=antivirus,intercept --quiet'
        'Synology Drive Client'                     = 'msiexec.exe /i "{0}" /qn /norestart'
        'Synology Active Backup for Business Agent' = 'msiexec.exe /i "{0}" ADDRESS={1} USERNAME={2} PASSWORD={3} /qn /norestart'
        'Sysmon64'                                  = '"{0}" -accepteula -i "{1}"'
        'TeamViewer'                                = 'msiexec.exe /i "{0}" /qn'
        'TeamViewer Host'                           = 'msiexec.exe /i "{0}" /qn CUSTOMCONFIGID="{1}"'
        'TreeSize Free'                             = '"{0}" /VERYSILENT /NORESTART'
        'Trend Micro Security Agent'                = 'msiexec.exe /i "{0}" SILENTMODE=1 /L*v+ "{1}" IDENTIFIER={2}'
        'UCS Client'                                = '"{0}" /s'
        'UniPrint'                                  = 'msiexec /q /i "{0}"'
        'VLC'                                       = '"{0}" /L=1033 /S'
        'Windirstat'                                = '"{0}" /S'
        'WinRAR'                                    = '"{0}" /S'
        'Wireshark'                                 = '"{0}" /S'
        'WinSCP'                                    = '"{0}" /VERYSILENT /NORESTART'
        'Yealink USB Connect'                       = 'msiexec.exe /i "{0}" /qn'
        'ZelloWork'                                 = 'msiexec /q /i "{0}" /norestart /Lime {1} ZELLO_SERVER="{2}"'
        'Zoom'                                      = 'msiexec.exe /i "{0}" /qn /norestart'
        

    }

    $installCommand = $InstallCommands.$ProgramName
    if (!$installCommand) { return $null }
    else { return $installCommand }
}

# Checks if program requested is valid to be installed/updated/uninstalled
Function Approve-SelectedProgram {
    Param (
        $ProgramName
    )

    $ApprovedPrograms = '7-Zip',
    'Adobe Acrobat',
    'Audacity',
    'Bitwarden',
    'CutePDF Writer',
    'Citrix Workspace',
    'Digisign',
    'Dropbox',
    'FileZilla',
    'Foxit PDF Reader',
    'GPL Ghostscript',
    'Google Chrome',
    'Google Drive',
    'IrfanView',
    'Jabra Direct',
    'Mozilla Firefox',
    'Notepad++',
    'PDF Creator',
    'Putty',
    'Python 3'
    'TreeSize Free',
    'UniPrint',
    'VLC',
    'Zoom'

    if ($ProgramName -notin $ApprovedPrograms) { return $null }
    return $true
}

function Convert-RegistryPathToShortForm {
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        # [Parameter(Mandatory = $true)]
        [string]$Path
    )

    # Define a hashtable for root key mapping
    $rootKeyMap = @{
        'Microsoft.PowerShell.Core\Registry::HKEY_CLASSES_ROOT'   = 'HKCR:'
        'Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER'   = 'HKCU:'
        'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE'  = 'HKLM:'
        'Microsoft.PowerShell.Core\Registry::HKEY_USERS'          = 'HKU:'
        'Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_CONFIG' = 'HKCC:'
    }

    # Convert the path
    foreach ($key in $rootKeyMap.Keys) {
        if ($path -like "$key*") {
            $path = $path -replace [regex]::Escape($key), $rootKeyMap[$key]
            break
        }
    }

    # Output the path
    return $path

}

Function Find-RootKeyFromPath {
    Param (
        $RegistryKey
    )

    # Matches rootKey from path using a regex
    $RootKeyRegex = '^(.+?)\:\\'
    if ($RegistryKey -match $RootKeyRegex) {
        $rootKey = $matches[1]
        # Write-Log -LogType INFO -Message "Root key: $rootKey"

        return $rootKey
    }

    Write-Log -LogType ERROR -Message "Unable to determine root key from provided path."
    return $null
}

Function Assert-RootKeyPSDrive {
    Param (
        $RegistryKey
    )

    $rootKey = Find-RootKeyFromPath -RegistryKey $RegistryKey
    if (!$rootKey) { return $null }

    # Checks if there is an existing PSDrive for RootKey
    $PSDrive = Get-PSDrive $rootKey -ErrorAction SilentlyContinue
    if (!($PSDrive)) {
        switch ($rootKey) {
            'HKCR' { $root = 'HKEY_CLASSES_ROOT' }
            'HKCU' { $root = 'HKEY_CURRENT_USER' }
            'HLKM' { $root = 'HKEY_LOCAL_MACHINE' }
            'HKU' { $root = 'HKEY_USERS' }
            'HKCC' { $root = 'HKEY_CURRENT_CONFIG' }
            default { 
                Write-Log -LogType ERROR -Message "Invalid root key."
                return $null
            }
        }
            
        Write-Log -LogType INFO -Message "Creating PSDrive for $rootKey."
        try {
            $newPSDrive = New-PSDrive -PSProvider registry -Root $root -Name $rootKey -Scope Global
            if ($newPSDrive) {
                Write-Log -LogType INFO -Message "PSDrive for $rootKey created."
                return $newPSDrive
            }
        }
        catch {
            Write-Warning "Error in creating PSDrive for $($rootKey):\ : $($Global:Error[0])"
            return $null
        }
    }

    #Write-Log -LogType INFO -Message "PSDrive for $rootKey is already available."
    return $PSDrive

}

Function Read-RegistryValueData {
    Param (
        $RegistryKey,
        $ValueName
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path -Path $RegistryKey)) {
        Write-Log -LogType INFO -Message "No matching registry keys with path $RegistryKey."
        return $null
    }

    $RegistryValueData = @()
    $matchedRegistryKeys = Get-ItemProperty -Path $RegistryKey #-Name $ValueName
    $matchedRegistryKeys | ForEach-Object { 
        $RegistryValueData += [PSCustomObject]@{
            Path  = $_.PSPath | Convert-RegistryPathToShortForm 
            Value = $_."$ValueName"
        }
    }

    return $RegistryValueData
}

Function Add-RegistryKey {
    Param (
        $RegistryKey
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    $components = $RegistryKey.Split("\")
    $currentPath = $components[0]

    for ($i = 1; $i -lt $components.Count; $i++) {
        $currentPath = $currentPath + "\" + $components[$i]
        if (!(Test-Path -Path $currentPath)) {
            try {
                $newRegistryKey = New-Item $RegistryKey -Force -ErrorAction Stop
                Write-Log -LogType INFO -Message "Registry key $RegistryKey created."
            }
            catch {
                Write-Log -LogType ERROR "Registry key $RegistryKey creation failed: $($Global:Error[0])"
                return $null
            }
        }
    }

    return $newRegistryKey
}

Function Add-RegistryValue {
    Param (
        $RegistryKey,
        $RegistryValueObj,
        $CreateKeyIfMissing = $true
    )

    #Write-Log -LogType INFO "Registry key: $RegistryKey"

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path $RegistryKey)) {
        if ($CreateKeyIfMissing) {
            $newRegistryKey = Add-RegistryKey -RegistryKey $RegistryKey
            if (!$newRegistryKey) { return $null }
        }
        else {
            Write-Log -LogType ERROR "Registry key $RegistryKey not found."
            return $null
        }
    }

    #foreach ($registryValueObj in $RegistryValueHash) {
    $ValueName = $registryValueObj.ValueName
    $ValueData = $registryValueObj.ValueData
    $ValueType = $registryValueObj.ValueType

    try {
        $newRegistryValue = New-ItemProperty -Path $RegistryKey -Name $ValueName -PropertyType $ValueType -ErrorAction Stop
        if ($newRegistryValue) {
            Write-Log -LogType INFO "Registry value $ValueName created as $ValueType."
        }
    }
    # Happens when registry value already exists.
    catch [System.IO.IOException] {
        # Do nothing
    }
    catch {
        Write-Log -LogType ERROR "Registry value $ValueName creation failed: $($Global:Error[0])"
    }

    try {
        $newRegistryValueData = Set-ItemProperty -Path $RegistryKey -Name $ValueName -Value $ValueData -PassThru -ErrorAction Stop
        if ($newRegistryValueData) {
            Write-Log -LogType INFO "Registry value $ValueName set to $ValueData."
        }
        return $newRegistryValueData
    }
    catch {
        Write-Log -LogType ERROR "Assigning value data to registry value $($registryValueObj.Name) failed: $($Global:Error[0])"
        return $null
    
    }

    #}
}

Function Remove-RegistryValue {
    Param (
        $RegistryKey,
        $ValueName
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path -Path $RegistryKey)) {
        Write-Log -LogType ERROR -Message "Registry key $RegistryKey not found."
    }

    try {
        Remove-ItemProperty $RegistryKey -Name $ValueName -Force -ErrorAction Stop
        Write-Log -LogType INFO "$ValueName value has been removed from $RegistryKey."
    }
    catch {
        Write-Log -LogType ERROR "Error removing value $ValueName in $RegistryKey : $($Global:Error[0])"
        return $null
    }   
}

Function Remove-RegistryKey {
    Param (
        $RegistryKey
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path -Path $RegistryKey)) {
        Write-Log -LogType INFO -Message "Registry key $RegistryKey not found."
        return $null
    }

    try {
        Remove-Item $RegistryKey -Force -ErrorAction Stop
        Write-Log -LogType INFO "$RegistryKey has been removed from the registry."
    }
    catch {
        Write-Log -LogType ERROR "Error removing $RegistryKey from the regsitry : $($Global:Error[0])"
        return $null
    }   
}

# Extracts a path from a string
Function Find-Path {
    Param (
        $String
    )
    
    $matches = @()
    $pathRegex = '[a-zA-Z]:\\(((?![<>:"\/\\|?*]).)+((?<![ .])\\)?)*'
    if (!($String -match $pathRegex)) { 
        Write-Log -LogType ERROR -Message "Unable to extract path from string."
        return $null 
    }
    
    $Path = $($matches[0]).Trim()
    return $Path
}
    
Function Get-PreInstallScriptBlock {
    Param($ProgramName)

    switch -regex($ProgramName) {
        'Adobe Digital Editions' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName'
                ScriptBlock = {

                    $N360RegistryKeys = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\N360', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\N360'
                    foreach ($key in $N360RegistryKeys) {
                        Write-Log -LogType INFO -Message "Creating registry key $key to skip Norton installation."
                        Add-RegistryKey -RegistryKey $key | Out-Null
                    }
                }
            }

            return $scriptBlockObj
        }
        'Arcserve ShadowProtect' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'savePath', 'licenseKey', 'clientName'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
                    
                    $installCommand = $($preInstallInputParams.installCommand)
                    $savePath = $($preInstallInputParams.savePath)
                    $licenseKey = $($preInstallInputParams.licenseKey)
                    $clientName = $($preInstallInputParams.clientName)

                    $installCommand = $installCommand -f '{0}', "$savePath\ArcserveShadowProtect.log", $licenseKey, $ENV:COMPUTERNAME, $clientName
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType DEBUG "Added log file, license key, name, and org to install command."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        '^CutePDF Writer$|^Digisign$|^Google Drive$|^Advanced IP Scanner$|^Sophos Endpoint Agent$|^TeamViewer$|^Ajax PRO Desktop' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $programName = $($preInstallInputParams.programName)
                    $installedProgram = Get-InstalledProgram $programName
                    if ($installedProgram) {
                        Write-Log -LogType INFO -Message "The older version of $programName will be uninstalled."
    
                        # Gets uninstall command
                        Write-Log -LogType INFO -Message "Retrieving uninstall command..."
                        $uninstallCommand = Get-UninstallCommand -InstalledProgram $installedProgram
                        if ($null -eq $uninstallCommand) { 
                            Write-Log -LogType ERROR -Message "Unable to retrieve uninstall command."
                            exit 1 
                        }
                        Write-Log -LogType DEBUG -Message "Uninstall command $uninstallCommand retrieved."
        
                        # Uninstalls the program
                        Write-Log -LogType INFO "Uninstalling $programName..."
                        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
                        if ($uninstallationResult -ne 0) {
                            Write-Log -LogType ERROR "Uninstallation failed. Process returned $uninstallationResult."
                            exit 1
                        }
                        
                        Write-Log -LogType DEBUG "Script will resume in 10 seconds to allow program to cleanly uninstall."
                        Start-Sleep 10
                    }
                    else {
                        Write-Log -LogType INFO "No old instance of $programName is installed."
                    }
                }
            }
            return $scriptBlockObj
        }
        'HP Support Assistant' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)

                    # Extracts installer
                    $extractCommand = '"{0}" /s /e /f "{1}"'
                    Write-Log -LogType INFO -Message "Extracting installer to $savePath..."
                    cmd /c $($extractCommand -f $installerLocation, $savePath)
                    Start-Sleep -Seconds 30

                    $installerFileName = 'InstallHPSA.exe'
                    $installerLocation = $savePath + "\$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'LandOnline Print-to-tiff Driver' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName', 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
                    $programName = $($preInstallInputParams.programName)

                    $installedProgram = Get-InstalledProgram $programName
                    if ($installedProgram) {
                        Write-Log -LogType INFO -Message "The older version of $programName will be uninstalled."
    
                        # Gets uninstall command
                        Write-Log -LogType INFO -Message "Retrieving uninstall command..."
                        $uninstallCommand = Get-UninstallCommand -InstalledProgram $installedProgram
                        if ($null -eq $uninstallCommand) { 
                            Write-Log -LogType ERROR -Message "Unable to retrieve uninstall command."
                            exit 1 
                        }
                        Write-Log -LogType DEBUG -Message "Uninstall command $uninstallCommand retrieved."
        
                        # Uninstalls the program
                        Write-Log -LogType INFO "Uninstalling $programName..."
                        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
                        if ($uninstallationResult -ne 0) {
                            Write-Log -LogType ERROR "Uninstallation failed. Process returned $uninstallationResult."
                            exit 1
                        }
                        
                        Write-Log -LogType DEBUG "Script will resume in 10 seconds to allow program to cleanly uninstall."
                        Start-Sleep 10
                    }
                    else {
                        Write-Log -LogType INFO "No old instance of $programName is installed."
                    }
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    #$savePath = $savePath + "\Admin"
                    $installerFileName = (Get-ChildItem -Path $savePath -Filter "LandOnlinePrintToTiff*.msi").Name
                    $installerLocation = $savePath + "\" + "$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $savePath
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'LegalAid Templates' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = (Get-ChildItem -Path $SavePath -Filter 'LegalAid Templates *.msi').Name
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'LOLComponents' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = 'LOLComponents.msi'
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Microsoft 365' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath', 'architecture', 'updateChannel', 'installCommand'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
                    $architecture = $($preInstallInputParams.architecture)
                    $udpateChannel = $($preInstallInputParams.updateChannel)
                    $installCommand = $($preInstallInputParams.installCommand)

                    $diskName = 'C'
                    $RequiredSpace = 30
                    $ConfigFileName = 'OfficeAppsConfiguration.xml'
                    
                    $architecture = if ( $architecture ) { $architecture }
                    else { 'x64' }

                    if ($architecture -eq 'x64') { $architecture = '64' }
                    else { $architecture = '32' }

                    Function Confirm-DiskSpaceRequirement {
                        Param (
                            $DiskName,
                            $RequiredSpace
                        )
                    
                        $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($DiskName):'" | Select-Object -Property DeviceID, @{'Name' = 'FreeGB'; Expression = { [int]($_.FreeSpace / 1GB) } }
                    
                        if ($disk.FreeGB -lt $RequiredSpace) {
                            return $false
                        }
                        return $true
                    }

                    Function Expand-OfficeDeploymentTool {
                        Param (
                            $ODTPath,
                            $ODTExtractPath
                        )
                    
                        if (Test-Path $ODTExtractPath) {
                            # If folder is existing, delete all contents inside it
                            try {
                                #Write-Host "$ExtractPath is existing. Removing existing contents." -NoNewline
                                Get-ChildItem -Path $ODTExtractPath -Include *.* -File -Recurse | ForEach-Object { $_.Delete() }
                            }
                            catch {
                                Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                                exit 1
                            }
                        }
                        else {
                            # Creates folder
                            try {
                                New-Item $ODTExtractPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
                            }
                            catch {
                                Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                                exit 1
                            }
                        }
                    
                        $cmd = '"{0}" /extract:"{1}" /quiet' -f $ODTPath, $ODTExtractPath
                        cmd /c $cmd
                    
                        return $(Get-Item $ODTExtractPath\setup.exe)
                    }

                    Function Write-OfficeConfigurationFile {
                        Param (
                            $Path,
                            $ConfigFileName,
                            $Architecture
                        )
                    
                        $configFilePath = "$Path\$ConfigFileName"
                        $configuration = @"
<Configuration>
    <Add SourcePath="$Path" OfficeClientEdition="$Architecture">
        <Product ID="O365BusinessRetail">
            <Language ID="en-us"/>
            <Language ID="MatchPreviousMSI"/>
            <ExcludeApp ID="Groove"/>
            <ExcludeApp ID="Lync"/>
            <ExcludeApp ID="Bing"/>
        </Product>
    </Add>
    <Property Name="FORCEAPPSHUTDOWN" Value="TRUE"/>
    <Property Name="PinIconsToTaskbar" Value="TRUE" />
    <Property Name="DeviceBasedLicensing" Value="0" />
    <Property Name="SCLCacheOverride" Value="0" />
    <Updates Enabled="TRUE" Channel="$udpateChannel"/>
    <Logging Level="Standard" Path="$Path\OfficeSetupLog.log"/>
    <RemoveMSI />
    <AppSettings>
        <Setup Name="Company" Value="Company" />
    </AppSettings>
</Configuration>
"@

                        
                        try {
                            New-Item $configFilePath -Force -ErrorAction Stop | Out-Null
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    
                        # Sets content to file
                        try {
                            Set-Content $configFilePath $configuration | Out-Null
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    
                        return $(Get-Item $configFilePath)
                    
                    }
                    
                    Write-Log -LogType INFO -Message "Checking disk space requirement..." 
                    $diskSpaceRequirement = Confirm-DiskSpaceRequirement -DiskName $diskName -RequiredSpace $RequiredSpace
                    if (!$diskSpaceRequirement) { 
                        Write-Log -LogType ERROR -Message "Installlation requires $RequiredSpace GB free in disk $DiskName. Please free up some space to continue."
                        exit 1 
                    }
                    Write-Log -LogType DEBUG -Message "There is sufficient disk space in $diskName to proceed."

                    Write-Log -LogType INFO -Message "Extracting office deployment tool..." 
                    $ODTExtractPath = $($SavePath + "\ODTSetup")
                    $installerLocation = Expand-OfficeDeploymentTool -ODTPath $installerLocation -ODTExtractPath $ODTExtractPath
                    if ($null -eq $installerLocation) { 
                        Write-Log -LogType ERROR -Message "Failed to extract office deployment tool."
                        exit 1 
                    }
                    Write-Log -LogType DEBUG -Message "Deployment tool extracted at $installerLocation."
                    
                    Write-Log -LogType INFO -Message "Creating Office365 configuration file..." 
                    $configurationFile = Write-OfficeConfigurationFile -Path $ODTExtractPath -ConfigFileName $ConfigFileName -Architecture $architecture
                    if ($null -eq $configurationFile) { 
                        Write-Log -LogType ERROR -Message "Failed to create configuration file."
                        exit 1 
                    }
                    Write-Log -LogType DEBUG -Message "Configuration file created at $configurationFile."

                    $cmd = '"{0}" /download "{1}\{2}"' -f $installerLocation, $ODTExtractPath, $ConfigFileName
                    Write-Log -LogType INFO "Downloading installation files using the command $cmd."
                    cmd /c $cmd
                    Write-Log -LogType INFO "Execution completed with exit code $LASTEXITCODE"
                    if ($LASTEXITCODE -ne 0) {
                        Write-Log -LogType ERROR "Unable to download installation files."
                        Remove-InstallerFolder -Path $SavePath -CleanupDelay $cleanupDelay
                        exit 1
                    }

                    $installCommand = $installCommand -f '{0}', $ODTExtractPath, $ConfigFileName #, $ConfigFileName
                    #$installerFileName = 'setup.exe'

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $ODTExtractPath
                        installerFileName = [System.IO.Path]::GetFileName($installerLocation)
                        installerLocation = $installerLocation
                        installCommand    = $installCommand
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj

        }
        'Patriot Task Service' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = (Get-ChildItem -Path $SavePath -Filter 'TaskService.msi').Name
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Patriot Version 6 Client' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = (Get-ChildItem -Path $SavePath -Filter 'Client.msi').Name
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Patriot Reporting Components' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = (Get-ChildItem -Path $SavePath -Filter 'PatriotReporting.msi').Name
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Philips Device Connector' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $savePath = $savePath + "\Windows"
                    $installerFileName = 'philips_device_connector_native_host_installer.exe'
                    $installerLocation = $savePath + "\" + "\$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $savePath
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Synology Active Backup for Business Agent' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'address', 'username', 'password'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
                    
                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installCommand = $($preInstallInputParams.installCommand)
                    $address = $($preInstallInputParams.address)
                    $username = $($preInstallInputParams.username)
                    $password = $($preInstallInputParams.password)

                    $installCommand = $installCommand -f '{0}', $address, $username, $password
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType INFO "Crafted install string based on NAS Address, username, and password."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Sysmon64' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName', 'installerLocation', 'savePath', 'installCommand', 'sysmonConfigSource'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
  
                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $registryKey = 'HKCU:\SOFTWARE\NZCS'
                    $registryValueName = 'SysmonConfigID'
                    $sysmon64ExePath = 'C:\WINDOWS\Sysmon64.exe'

                    $programName = $($preInstallInputParams.programName)
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
                    $installCommand = $($preInstallInputParams.installCommand)
                    $sysmonConfigSource = $($preInstallInputParams.sysmonConfigSource)
                    # $configAction = $($preInstallInputParams.configAction)

                    Function Get-CurrentSysmonConfig {
                        Param (
                            $RegistryKey,
                            $RegistryValueName
                        )

                        $SysmonCurrentConfig = Read-RegistryValueData -RegistryKey $RegistryKey -ValueName $RegistryValueName

                        switch ($SysmonCurrentConfig.Value) {
                            '2' {
                                return 'Standard'
                            }
                            '4' {
                                return 'Enhanced'
                            }
                            default {
                                Write-Log -LogType WARNING "Unable to determine current Sysmon config type. Using Standard configuration."
                                return 'Standard'
                            }
                        }
                    }

                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Log -LogType WARNING -Message "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    #$savePath = $savePath + "\Admin"
                    $installerFileName = (Get-ChildItem -Path $savePath -Filter "Sysmon64.exe").Name
                    $installerLocation = $savePath + "\" + "$installerFilename"

                    Write-Log -LogType DEBUG -Message "Extraction Complete."

                    # if ($configAction -eq 'Update' -or $null -eq $configAction) {
                                
                    $standardConfig = 'https://raw.githubusercontent.com/SwiftOnSecurity/sysmon-config/master/sysmonconfig-export.xml'
                    $enhancedConfig = 'https://raw.githubusercontent.com/Neo23x0/sysmon-config/master/sysmonconfig-export-block.xml'

                    if ($sysmonConfigSource -eq 'Use Current') {
                        Write-Log -LogType INFO -Message "Current config type be refreshed from its source."
                        $sysmonConfigSource = Get-CurrentSysmonConfig -RegistryKey $RegistryKey -RegistryValueName $RegistryValueName
                    }

                    Write-Log -LogType INFO -Message "Installation will use $sysmonConfigSource Sysmon configuration."
                        
                    switch ($sysmonConfigSource) {
                        'Standard' {
                            $configFileLink = $standardConfig
                            $sysmonConfigID = 2
                        }
                        'Enhanced' {
                            $configFileLink = $enhancedConfig
                            $sysmonConfigID = 4
                        }
                        default {
                            Write-Log -LogType INFO -Message "Unnkown sysmon config source selected. Using Standard configuration."
                            $configFileLink = $standardConfig
                            $sysmonConfigID = 2
                        }
                    }
    
                    Write-Log -LogType DEBUG -Message "Sysmon config source set to $configFileLink"
    
                    Write-Log -LogType INFO -Message "Downloading Sysmon64 config file."
                    $configFileLocation = Get-Installer -DownloadLink $configFileLink -SavePath $SavePath
                    if ($null -eq $configFileLocation) { exit 1 }
                    Write-Log -LogType DEBUG -Message "Config file saved at $configFileLocation."

                    $installCommand = $installCommand -f '{0}', $configFileLocation

                    $registryValueObj += [PSCustomObject]@{
                        ValueName = $registryValueName
                        ValueData = $sysmonConfigID
                        ValueType = 'DWORD'
                    }
                    Add-RegistryValue -RegistryKey $registryKey -RegistryValueObj $registryValueObj -CreateKeyIfMissing $true | Out-Null

                    # $installedService = Get-InstalledService -ServiceDisplayName $ProgramName
                    if (Test-Path -Path $sysmon64ExePath) {
                        Write-Log -LogType INFO -Message "Removing existing Sysmon64 installation."

                        $uninstallCommand = '"{0}" -u force' -f $installerLocation
                        cmd /c "$uninstallCommand"  > $null
                    }

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $savePath
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                        installCommand    = $installCommand
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        # '^TeamViewer$' {
        # $scriptBlockObj = [PSCustomOBject]@{
        # Params = 'installerLocation', 'savePath'
        # ScriptBlock = {
        # Param (
        # $preInstallInputParams
        # )
        
        # $installerLocation = $($preInstallInputParams.installerLocation)
        # $savePath = $($preInstallInputParams.savePath)
        
        # # Write-Log -LogType INFO -Message "Test"
        # Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
        # try {
        # Add-Type -Assembly "System.IO.Compression.FileSystem"
        # [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
        # # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
        # }
        # catch {
        # Write-Warning "The script ran into an issue: $($Global:Error[0])"
        # exit 1
        # }
                    
        # $savePath = $savePath + "\Full"
        # $installerFileName = (Get-ChildItem -Path $savePath -Filter "TeamViewer_Full.msi").Name
        # $installerLocation = $savePath + "\" + "\$installerFilename"

        # $preInstallOutputObject = [PSCustomObject]@{
        # savePath = $savePath
        # installerFileName = $installerFileName
        # installerLocation = $installerLocation
        # }
        # Write-Log -LogType INFO -Message "Extraction Complete."
        
        # return $preInstallOutputObject
        # }
        # }
        # return $scriptBlockObj
        # }
        '^TeamViewer Host$' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'customConfigID'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
                
                    $installCommand = $($preInstallInputParams.installCommand)
                    $customConfigID = $($preInstallInputParams.customConfigID)

                    $installCommand = $installCommand -f '{0}', $customConfigID
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType INFO "Added customConfigID to install command."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Trend Micro Security Agent' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'savePath', 'clientID'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
                    
                    $installCommand = $($preInstallInputParams.installCommand)
                    $savePath = $($preInstallInputParams.savePath)
                    $clientID = $($preInstallInputParams.clientID)

                    $installCommand = $installCommand -f '{0}', "$savePath\$global:programNameNoSpace.log", $clientID
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType INFO "Crafted install command based on log file and Client ID."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'UCS Client' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath', 'programName'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $programName = $($preInstallInputParams.programName)
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)

                    $installedProgram = Get-InstalledProgram $programName
                    if ($installedProgram) {
                        Write-Log -LogType INFO -Message "The older version of $programName will be uninstalled."

                        # Terminates related processes before updating program
                        Write-Log -LogType INFO "Terminating $programName processes before uninstalling..." -NoNewline
                        try {
                            $processesToTerminate = Get-ProcessToTerminate -ProgramName $programName
                            Get-Process $processesToTerminate.Name | Stop-Process -Force -ErrorAction Continue
                        }
                        catch {
                            Write-Log -LogType ERROR "Unable to terminate processes: $($Error[0])"
                        }
    
                        # Gets uninstall command
                        Write-Log -LogType DEBUG -Message "Retrieving uninstall command..."
                        $uninstallCommand = Get-UninstallCommand -InstalledProgram $installedProgram #-UseStringsExe


                        if ($null -eq $uninstallCommand) { 
                            Write-Log -LogType ERROR -Message "Unable to retrieve uninstall command."
                            exit 1 
                        }
                        Write-Log -LogType DEBUG -Message "Uninstall command $uninstallCommand retrieved."
        
                        # Uninstalls the program
                        Write-Log -LogType INFO "Uninstalling $programName..."
                        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
                        if ($uninstallationResult -ne 0) {
                            Write-Log -LogType ERROR "Uninstallation failed. Process returned $uninstallationResult."
                            exit 1
                        }
                        
                        Write-Log -LogType DEBUG "Script will resume in 10 seconds to allow program to cleanly uninstall."
                        Start-Sleep 10
                    }
                    else {
                        Write-Log -LogType INFO "No old instance of $programName is installed."
                    }
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = 'setup.exe'
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'UniPrint' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $savePath = $savePath + "\Admin"
                    $installerFileName = (Get-ChildItem -Path $savePath -Filter "UniPrintClient_*.msi").Name
                    $installerLocation = $savePath + "\" + "\$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $savePath
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'ZelloWork' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'savePath', 'zelloServer'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $preInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }
                    
                    $installCommand = $($preInstallInputParams.installCommand)
                    $savePath = $($preInstallInputParams.savePath)
                    $zelloServer = $($preInstallInputParams.zelloServer)

                    $installCommand = $installCommand -f '{0}', "$savePath\ZelloWorkClientInstall.log", $zelloServer
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType INFO "Added log file and Zello server to install command."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
    }
}

Function Get-PostInstallScriptBlock {
    Param($ProgramName)

    switch -regex($ProgramName) {
        'Arcserve ShadowControl' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'ipAddress', 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $ipAddress = $($postInstallInputParams.ipAddress)
                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    $installLocation = (Get-InstalledProgram 'Arcserve ShadowControl').InstallLocation
                    
                    if (!$installLocation) {
                        Write-Log -LogType ERROR "Unable to retrieve install location. Please check for issues."
                        return $null
                    }

                    if (!$ipAddress) {
                        Write-Log -LogType INFO "IP Address not supplied. Skipping appliance subscription."
                        return $null
                    }

                    Write-Log -LogType INFO "Subscribing to an applicance."
                    $command = '"{0}stccmd" subscribe {1}' -f $installLocation, $ipAddress

                    Write-Log -LogType DEBUG "Executing command $command"
                    cmd /c $command

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Patriot Task Service' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    Write-Log -LogType INFO "Verifying consistency between registry version and program exe version."
                    
                    $programName = 'Patriot Task Service' # why declare again?
                    $programExeFileName = 'CSMService.exe'

                    $installedProgram = Get-InstalledProgram -RegistryDisplayName $programName
                    $installLocation = $installedProgram.InstallLocation
                    $programExeFilePath = $installLocation + $programExeFileName
                    
                    $registryVersion = $installedProgram.DisplayVersion
                    $programExeVersion = (Get-Command $programExeFilePath).FileVersionInfo.FileVersion

                    $isProgramVersionsConsistent = [version]$registryVersion -eq [version]$programExeVersion
                    if (!$isProgramVersionsConsistent) {
                        Write-Log -LogType ERROR "Versions in registry and in program executable file does not match. Registry: $registryVersion; Exe: $programExeVersion. Please check for issues."

                        return $null
                    }
                    else {
                        Write-Log -LogType DEBUG "Versions in registry and in program executable file are matching. Registry: $registryVersion; Exe: $programExeVersion."
                    }

                    Write-Log -LogType INFO "Confirming service."

                    $serviceDisplayName = 'Patriot Task Service'
                    $installedService = Get-InstalledService -ServiceDisplayName $ServiceDisplayName

                    if (!$installedService) {
                        Write-Log -LogType ERROR "Service $serviceDisplayName is not installed. Please check for issues and reinstall."
                        return $null
                    }

                    Write-Log -LogType INFO "Starting service $serviceDisplayName."
                    try {
                        $startedService = Start-Service -Name $serviceDisplayName -PassThru
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }

                    if ($startedService.Status -eq 'Running') {
                        Write-Log -LogType INFO "Service $serviceDisplayName is now running."
                    }


                }
            }
            return  $scriptBlockObj
        }
        'Patriot Version 6 Client' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    Write-Log -LogType INFO "Verifying consistency between registry version and program exe version."
                    
                    $programName = 'Patriot Version 6 Client' # why declare again?
                    $programExeFileName = 'PatriotV6Client.exe'

                    $installedProgram = Get-InstalledProgram -RegistryDisplayName $programName
                    $installLocation = $installedProgram.InstallLocation
                    $programExeFilePath = $installLocation + $programExeFileName
                    
                    $registryVersion = $installedProgram.DisplayVersion
                    $programExeVersion = (Get-Command $programExeFilePath).FileVersionInfo.FileVersion

                    $isProgramVersionsConsistent = [version]$registryVersion -eq [version]$programExeVersion
                    if (!$isProgramVersionsConsistent) {
                        Write-Log -LogType ERROR "Versions in registry and in program executable file does not match. Registry: $registryVersion; Exe: $programExeVersion. Please check for issues."

                        return $null
                    }

                    Write-Log -LogType DEBUG "Versions in registry and in program executable file are matching. Registry: $registryVersion; Exe: $programExeVersion."
                }
            }
            return  $scriptBlockObj
        }
        'Patriot Reporting Components' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    Write-Log -LogType INFO "Verifying consistency between registry version and program exe version."
                    
                    $programName = 'Patriot Reporting Components'
                    $programExeFileName = 'ReportViewer.exe'

                    $installedProgram = Get-InstalledProgram -RegistryDisplayName $programName
                    $installLocation = $installedProgram.InstallLocation
                    $programExeFilePath = $installLocation + $programExeFileName
                    
                    $registryVersion = $installedProgram.DisplayVersion
                    $programExeVersion = (Get-Command $programExeFilePath).FileVersionInfo.FileVersion

                    $isProgramVersionsConsistent = [version]$registryVersion -eq [version]$programExeVersion
                    if (!$isProgramVersionsConsistent) {
                        Write-Log -LogType ERROR "Versions in registry and in program executable file does not match. Registry: $registryVersion; Exe: $programExeVersion. Please check for issues."

                        return $null
                    }

                    Write-Log -LogType DEBUG "Versions in registry and in program executable file are matching. Registry: $registryVersion; Exe: $programExeVersion."
                }
            }
            return  $scriptBlockObj
        }
        'Philips Device Connector' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    $ProgramName = 'Philips Device Connector'
                    $browserNames = 'Google Chrome'#, 'Microsoft Edge'

                    foreach ($browserName in $browserNames) {
                        $installedProgram = Get-InstalledProgram $browserName
                        if (!$installedProgram) {
                            Write-Log -LogType INFO -Message "$browserName is not installed in this system."
                            continue
                        }

                        $extensionID = Get-BrowserExtensionID -BrowserName $browserName -ExtensionName $ProgramName
                        if (!$extensionID) {
                            Write-Log -LogType ERROR -Message "Unable to find $ProgramName's extension ID for $browserName."
                            continue
                        }

                        Add-BrowserExtension -BrowserName $browserName -ExtensionID $extensionID
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'ServiceCATRMM' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."

                    Write-Log -LogType INFO "Confirming service."

                    $serviceDisplayName = 'SCService'
                    $installedService = Get-InstalledService -ServiceDisplayName $ServiceDisplayName

                    if (!$installedService) {
                        Write-Log -LogType ERROR "Service is not installed. Please check for issues and reinstall."
                        return $null
                    }

                    Write-Log -LogType INFO "Starting service."
                    try {
                        $startedService = Start-Service -Name $serviceDisplayName -PassThru
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }

                    if ($startedService.Status -eq 'Running') {
                        Write-Log -LogType INFO "Service $serviceDisplayName is now running."
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Sysmon64' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."
                    
                    $events = Get-WinEvent -ListLog Microsoft-Windows-Sysmon*

                    if (!$events) {
                        Write-Log -LogType WARNING "Sysmon event logs were not created. Please check for issues."
                    }

                    Write-Log -LogType INFO "Sysmon event logs successfully verified."

                    $sysmonExePath = 'C:\Windows\Sysmon64.exe'
                    $sysmonExeFile = Get-Item $sysmonExePath -ErrorAction Ignore

                    if (!$sysmonExeFile) {
                        Write-Log -LogType ERROR "Unable to to find file in $sysmonExePath."
                    }
                    else {
                        Write-Log -LogType INFO "Sysmon file version: $($sysmonExeFile.VersionInfo.FileVersion)"
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        '^TeamViewer Host$' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'ApiToken', 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $ApiToken = $($postInstallInputParams.ApiToken)
                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {
                        Write-Log -LogType INFO "Skipping post-install steps."
                        return $null
                    }

                    Write-Log -LogType INFO -Message "Performing post-install steps."
                    
                    $teamViewerExecutablePath = "C:\Program Files (x86)\TeamViewer\TeamViewer.exe" 
                    $teamViewerArgsList = "assign --api-token $ApiToken --grant-easy-access"
                    
                    Write-Log -LogType INFO "Starting TeamViewer."
                    #Start-Process -FilePath $teamViewerExecutablePath -ArgumentList $teamViewerArgsList -Wait
                    $command = '"{0}" {1}' -f $teamViewerExecutablePath, $teamViewerArgsList

                    Write-Log -LogType DEBUG "Executing command $command"
                    cmd /c $command

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Trend Micro Security Agent' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'savePath', 'installStatus'
                ScriptBlock = {
                    Param (
                        $postInstallInputParams
                    )

                    $postInstallInputParams.PSObject.Properties | ForEach-Object {
                        if (!($_.Value)) {
                            Write-Log -LogType WARNING "Variable $($_.Name) is null."
                        }
                    }

                    $savePath = $($postInstallInputParams.savePath)
                    $installStatus = $($postInstallInputParams.installStatus)

                    if ($installStatus -eq 'Failed') {

                        Write-Log -LogType INFO -Message "Performing post-install steps."
                        
                        Write-Log -LogType INFO "Retrieving last 10 lines of installation log file."

                        $logFilePath = "$savePath\$global:programNameNoSpace.log"
                        
                        if (!(Test-Path $logFilePath)) {
                            Write-Log -LogType ERROR "Unable to find log file $logFilePath."
                            return $null
                        }
    
                        $content = Get-Content $logFilePath | Select-Object -Last 10
                        if (!$content) {
                            Write-Log -LogType INFO "Log file is empty."
                        }
    
                        Write-Host $content
                    }                    

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
    }
}

Function Get-ProcessToTerminate {
    Param (
        $ProgramName
    )

    $ProcessesToTerminate = @{
        '7-Zip'                 = '*7z*'
        'Adobe Acrobat'         = '*Acrobat*'
        'Audacity'              = '*Audacity*'
        'BitWarden'             = '*Bitwarden*'
        'Citrix Workspace'      = '*Citrix*', '*Receiver*', '*SelfService*'
        'CutePDF Writer'        = '*CutePDF*'
        'Digisign'              = '*Digisign*'
        'Dropbox'               = '*Dropbox*'
        'Microsoft Edge'        = '*msedge*'
        'Filezilla'             = '*filezilla*'
        'Foxit PDF Reader'      = '*Foxit*'
        'Github Desktop'        = 'GithubDesktop*'
        'Google Drive'          = '*GoogleDrive*'
        'GPL Ghostscript'       = '*gswin*'
        'Google Chrome'         = '*chrome*'
        'IrfanView'             = '*i_view*'
        'Jabra Direct'          = '*jabra-direct*'
        'LOLComponents'         = '*LOLComponents*'
        'LegalAid Templates'    = '*LegalAid Templates*'
        'Mozilla Firefox'       = '*firefox*'
        'Notepad++'             = '*notepad++*'
        'PDFCreator'            = '*PDFCreator*'
        'Putty'                 = '*putty*'
        'Python'                = '*Python*'
        'Snagit'                = '*snagit*'
        'Synology Drive Client' = '*cloud-drive-ui.exe*'
        'TreeSize Free'         = '*TreeSizeFree*'
        'UCS Client'            = 'UCS_Client'
        'UniPrint'              = '*UPC*'
        'VLC'                   = '*VLC*'
        'Wireshark'             = '*Wireshark*'
        'Zoom'                  = '*Zoom*'
    }

    $ProcessesToTerminate = [PSCustomObject]@{
        Name = $ProcessesToTerminate.$ProgramName
    }
    # if (!$ProcessesToTerminate) { return $ProgramName }
    # else { return $ProcessesToTerminate }
    return $ProcessesToTerminate
}



Function Get-LowerVersionConflicts {
    Param (
        $ProgramName
    )

    $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName

    $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All
    
    # Get the latest version installed
    #$latestVersion = [System.Version]::Parse(($installedPrograms | Sort-Object { [System.Version]::Parse($_.DisplayVersion) } -Descending)[0].DisplayVersion)
    $latestVersion = $installedPrograms[0].DisplayVersion

    # Filter out the objects with the latest version
    try {
        $lowerVersionConflicts = $installedPrograms | Where-Object { [System.Version]::Parse($_.DisplayVersion) -ne $latestVersion } -ErrorAction Stop
    }
    catch {
        Write-Log -LogType WARNING -Message "$($Global:Error[0])"
    }

    return $lowerVersionConflicts
}


Function Uninstall-LowerVersionConflicts {
    Param (
        $ProgramName
    )

    $lowerVersionConflicts = Get-LowerVersionConflicts -ProgramName $ProgramName

    if (!$lowerVersionConflicts) {
        Write-Log -LogType DEBUG -Message "No installations of $programName on lower versions detected."
        return
    }
    
    # If there are, uninstall them
    Write-Log -LogType INFO -Message "Installations of $programName on lower versions detected."

    :OuterLoop foreach ($installation in $lowerVersionConflicts) {
        Write-Log -LogType INFO -Message "$($installation.DisplayName) $($installation.DisplayVersion) ($($installation.Architecture)) will be uninstalled."
        
        Write-Log -LogType INFO -Message "Retrieving uninstall command."
        $uninstallCommand = $installation | Get-UninstallCommand # -UseStringsExe
 
        # Uninstalls the program
        Write-Log -LogType INFO "Uninstalling $($installation.DisplayName)..."
        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
        
        if ($uninstallationResult -ne 0) {
            Write-Log -LogType ERROR "Uninstallation failed."
        }
        else {
 
            # After uninstalling the script will do another check if there are still installations on a different architecture existing.
            $delay = 10
            Write-Log -LogType DEBUG -Message "Performing another check after uninstalling in $delay seconds."
            Start-Sleep -Second $delay

            $lowerVersionConflicts_recheck = Get-LowerVersionConflicts -ProgramName $ProgramName
         
            # If duplicates are still found, it will continue to try uninstalling the remaining installations
            if (!$lowerVersionConflicts_recheck) {
                Write-Log -LogType INFO -Message "Lower version conflicts removed."
                return     
            }
                 
            Write-Log -LogType INFO -Message "Lower version conflicts still found."
        }
    }
}


Function Get-InstallationsArchitecture {
    Param (
        $InstalledPrograms
    )

    foreach ($program in $InstalledPrograms) {
        $installedArchitecture = $program | Get-ProgramArchitecture

        $program | Add-Member -Name "Architecture" -Value $installedArchitecture -MemberType NoteProperty
    }

    return $InstalledPrograms

}

Function Get-ArchitectureConflicts {
    Param (
        $ProgramName,
        $Architecture
    )

    $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName

    $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All

    # Gets current installations of program in a different architecture
    $architectureConflicts = $installedPrograms | Where-Object { ($_.Architecture -ne $Architecture) -and ($null -ne $_.Architecture) }

    return $architectureConflicts
}

Function Uninstall-ArchitectureConflicts {
    Param (
        $ProgramName,
        $Architecture
    )

    $architectureConflicts = Get-ArchitectureConflicts -ProgramName $ProgramName -Architecture $Architecture


    if (!$architectureConflicts) {
        Write-Log -LogType DEBUG -Message "No installations of $programName on a different architecture have been detected."
        return
    }
 
    # If there are, uninstall them
    Write-Log -LogType INFO -Message "Installations of $programName on a different architecture have been detected."
             
    # Loops through each installation and uninstalls.
    :OuterLoop foreach ($installation in $architectureConflicts) {
        Write-Log -LogType INFO -Message "$($installation.DisplayName) ($($installation.Architecture)) will be uninstalled."
        Write-Log -LogType INFO -Message "Retrieving uninstall command."
        $uninstallCommand = $installation | Get-UninstallCommand # -UseStringsExe
 
        # Uninstalls the program
        Write-Log -LogType INFO "Uninstalling $($installation.DisplayName)..."
        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
        if ($uninstallationResult -ne 0) {
            Write-Log -LogType ERROR "Uninstallation failed."
        }
        else {
 
            # After uninstalling the script will do another check if there are still installations on a different architecture existing.
            $delay = 10
            Write-Log -LogType DEBUG -Message "Performing another check after uninstalling in $delay seconds."
            Start-Sleep -Second $delay
 
            # $installedPrograms_recheck = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All | Where-Object { ($_.Architecture -ne $Global:ArchitectureUsed) -and ($null -ne $_.Architecture) }
         
            $architectureConflicts_recheck = Get-ArchitectureConflicts -ProgramName $ProgramName -Architecture $Architecture

            # If duplicates are still found, it will continue to try uninstalling the remaining installations
            if (!$architectureConflicts_recheck) {
                Write-Log -LogType INFO -Message "Architecture conflicts removed."
                return     
            }
                 
            Write-Log -LogType INFO -Message "Architecture conflicts still found."
        }
    }
}

Function Approve-Installation {
    Param (
        $ProgramName,
        $Force
    )

    $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName
    
    $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All

    # If installation requires the architecture to be specified, look for installations on the same architecture
    if ($Global:ArchitectureUsed) {
        $installedPrograms = $installedPrograms | Where-Object { ($_.Architecture -eq $Global:ArchitectureUsed) -and ($null -ne $_.Architecture) }
    }

    if ($global:scriptName -like "Install*") {
        # If there's no installation found on the same architecture, resolve possible architecture conflicts then install
        if (!$installedPrograms) {
            Write-Log -LogType INFO -Message "No existing installation of $programName $Global:ArchitectureUsed found."
           
            return $true
        }

        # If there are found, evaluate if installation should proceed based on the Force switch
        Write-Log -LogType INFO -Message "$programName $($installedPrograms[0].DisplayVersion) ($($installedPrograms[0].Architecture)) is already installed in this system."
        
        # If force is not specified or set to false, installation will not proceed
        if ((!$force) -or ($force -eq 'false')) {
            Write-Log -LogType INFO -Message "Installation will not proceed."
            return $false
        }

        # If force is True, resolve possilble architecture conflicts then install
        Write-Log -LogType DEBUG -Message "Force switch is set to True. Installation will proceed."
       
        return $true
        
        
    }
    elseif ($global:scriptName -like "Update*") {
        
        # If there are installations found on the same architecture, resolve possible architecture conflicts then update
        if ($installedPrograms) {
            Write-Log -LogType INFO -Message "$programName $Global:ArchitectureUsed is installed in this system."
            
            $installedVersion = $installedPrograms[0].DisplayVersion
            $isProgramForUpdate = Confirm-Update -ProgramName $programName -InstalledVersion $installedVersion -VersionMatchRegex $versionMatchRegex

            if ($isProgramForUpdate) {
                return $true
            }

            if ((!$force) -or ($force -eq 'false')) {
                Write-Log -LogType INFO -Message "Update will not proceed."
                return $false
            }

            Write-Log -LogType DEBUG -Message "Force switch is set to True. Update will proceed."
          
            return $true
        }
        
        # If there are found, evaluate if installation should proceed based on the Force switch
        Write-Log -LogType INFO -Message "No existing installation of $programName $Global:ArchitectureUsed found."

        # If force is not specified or set to false, installation will not proceed
        if ((!$force) -or ($force -eq 'false')) {
            Write-Log -LogType INFO -Message "Nothing to update."
            return $false
        }
       
        # If force is True, resolve possilble architecture conflicts then update
        Write-Log -LogType DEBUG -Message "Force switch is set to True. Update will proceed."
       
        return $true
    }
}

Function Approve-ProgramInstall {
    Param (
        $ProgramName,
        $Architecture,
        $ProgramType,
        $Force
    )

    if ($Force -eq 'true') {
        Write-Log -LogType INFO -Message "Force switch is set to True. Installation will proceed."              
        return $true
    }

    switch ($ProgramType) {
        'Application' {
            $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName
    
            $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All
        
            # If installation requires the architecture to be specified, look for installations on the same architecture
            if ($Architecture) {
                $installedPrograms = $installedPrograms | Where-Object { ($_.Architecture -eq $Architecture) -and ($null -ne $_.Architecture) }
            }

            if ($installedPrograms) {
                Write-Log -LogType INFO -Message ('{0} is already installed in this system.' -f $(Show-ProgramQuickInfo -Program $installedPrograms[0]))
                break
            }

            Write-Log -LogType INFO -Message "No existing installation of $programName $Architecture found. Installation will proceed."
            
            return $true
            
        }
        'Service' {
            $installedService = Get-InstalledService -ServiceDisplayName $programName

            if (!$installedService) {
                Write-Log -LogType INFO -Message "No existing installation of service $programName found. Installation will proceed."
                return $true
            }

            # If there are found, evaluate if installation should proceed based on the Force switch
            Write-Log -LogType INFO -Message "Service $programName is already installed in this system."
            break
        }
        default {
            Write-Log -LogType ERROR -Message "Program type not specified."
        }

    }

    Write-Log -LogType INFO -Message "Installation will not proceed."
    return $false
}

Function Approve-ProgramUpdate {
    Param (
        $ProgramName,
        $Architecture,
        $ProgramType,
        $LatestVersionAvailable,
        $Force
    )

    if ($Force -eq 'true') {
        Write-Log -LogType INFO -Message "Force switch is set to True. Update will proceed."              
        return $true
    }

    switch ($ProgramType) {
        'Application' {
            $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName
    
            $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All
        
            # If installation requires the architecture to be specified, look for installations on the same architecture
            if ($Architecture) {
                $installedPrograms = $installedPrograms | Where-Object { ($_.Architecture -eq $Architecture) -and ($null -ne $_.Architecture) }
            }
        
            if (!$installedPrograms) {
                Write-Log -LogType INFO -Message "No existing installation of $programName $Architecture found."
                break
            }
        
            Write-Log -LogType INFO -Message ('{0} is installed in this system.' -f $(Show-ProgramQuickInfo -Program $installedPrograms[0]))



            $installedVersion = $installedPrograms[0].DisplayVersion

            Write-Log -LogType INFO -Message "Version number: $installedVersion"
            Write-Log -LogType INFO -Message "Latest available: $LatestVersionAvailable"
            
            $isProgramForUpdate = Confirm-Update -ProgramName $programName -InstalledVersion $installedVersion -LatestVersionAvailable $LatestVersionAvailable -VersionMatchRegex $versionMatchRegex

            if ($isProgramForUpdate) {
                Write-Log -LogType INFO -Message "Update will proceed." 
                return $true
            }

            #Write-Log -LogType INFO -Message "Nothing to update."
            break
        }
        'Service' {
            $installedService = Get-InstalledService -ServiceDisplayName $programName

            if ($installedService) {
                Write-Log -LogType INFO -Message "Service $programName is installed in this system. Update will proceed."
                return $true
            }

            Write-Log -LogType INFO -Message "Service $programName is not installed in this system."
            break
        }
        default {
            Write-Log -LogType ERROR -Message "Program type not specified."
        }

    }

    Write-Log -LogType INFO -Message "Update will not proceed."
    return $false
}

Function Get-M365SupportedVersion {
    Param (
        $UpdateChannel
    )

    $UpdateChannel = $([Regex]::Escape($UpdateChannel)) 
    
    $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
    $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
    $AllMatches = ([regex]$Pattern).Matches($HTML)
    $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
    return $latestVersion
}

Function Get-M365UpdateChannel {
    $RegistryKey = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration'
    $ValueName = 'AudienceID'

    $UpdateChannels = @{
        '492350f6-3a01-4f97-b9c0-c7c6ddf67d60' = 'Current Channel'
        '55336b82-a18d-4dd6-b5f6-9e5095c314a6' = 'Monthly Enterprise Channel'
        'b8f9b850-328d-4355-9145-c59439a0c4cf' = 'Semi-Annual Enterprise Channel (Preview)'
        '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' = 'Semi-Annual Enterprise Channel'
    }

    $audienceID = (Read-RegistryValueData -RegistryKey $RegistryKey -ValueName $ValueName).Value
    if (!$audienceID) {
        return $null
    }

    $updateChannel = $UpdateChannels.$audienceID
    if (!$updateChannel) {
        Write-Log -LogType ERROR "Unable to find corresponding update channel for AudienceID $audienceID."
        return $null
    }

    return $updateChannel
}

Function Show-ProgramQuickInfo {
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $Program
    )

    $version = if ($program.DisplayVersion) { " $($program.DisplayVersion)" } else { "" }
    $architecture = if ($program.Architecture) { " ($($program.Architecture))" } else { "" }

    return ('{0}{1}{2}' -f $program.DisplayName, $version, $architecture)
}

Function Get-BrowserExtensionID {
    Param (
        $ExtensionName,
        $BrowserName
    )

    $extensionDataJson = @"
{
    "uBlock Origin" : {
        "Google Chrome" : "cjpalhdlnbpafiamejdnhcphjbkeiagm",
        "Microsoft Edge" : "odfafepnkmbhccpbejgmiehpchacaeak"
    },
    "Bitwarden - Free Password Manager" : {
        "Google Chrome" : "nngceckbapebfimnlniiiahkandclblb",
        "Microsoft Edge" : "jbkfoedolllekgbhcbcoahefnbanhhlh"
    },
    "Philips Device Connector" : {
        "Google Chrome" : "okdioccnnkeanlegkfgnnboncggagfjh"
    }
}
"@


    $extensionData = $extensionDataJson | ConvertFrom-Json
    return $extensionData.$ExtensionName.$BrowserName

}

Function Resolve-BrowserExtensionID {
    Param (
        $BrowserName, $ExtensionID
    )

    $validateURLs = @{
        'Google Chrome'  = 'https://chrome.google.com/webstore/detail/{0}/?hl=en-US' -f $ExtensionID
        'Microsoft Edge' = 'https://microsoftedge.microsoft.com/addons/getproductdetailsbycrxid/{0}' -f $ExtensionID
    }

    $validateURL = $validateURLs.$BrowserName
    if (!$validateURL) {
        Write-Log -LogType ERROR "Invalid browser name."
        return $null
    }

    try {
        $browserExtensionInfo = Invoke-WebRequest -UseBasicParsing -URI $validateURL -ErrorAction Stop
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to get browser extension info: $($Global:Error[0])"
        return $null
    }

    if (!$browserExtensionInfo) {
        Write-Log -LogType ERROR -Message "No info generated for browser extension ID $ExtensionID"
        return $null
    }

    switch ($BrowserName) {
        'Google Chrome' {
            $HTML = $browserExtensionInfo.Content
            $Pattern = '<title>(?<name>.+?) - Chrome Web Store</title>'
            $AllMatches = ([regex]$Pattern).Matches($HTML)
            $browserExtensionName = ($AllMatches[0].Groups.Where{ $_.Name -like 'name' }).Value
            
            Add-Type -AssemblyName System.Web
            $browserExtensionName = [System.Web.HttpUtility]::HtmlDecode($browserExtensionName)
            return $browserExtensionName
        }
        'Microsoft Edge' {
            return (($browserExtensionInfo).Content | ConvertFrom-Json).Name
        }
    }
    
}

Function Add-BrowserExtension {
    Param (
        $BrowserName, $ExtensionID
    )

    $extensionName = Resolve-BrowserExtensionID -BrowserName $browserName -ExtensionID $extensionId
    if (!$extensionName) {
        Write-Log -LogType ERROR -Message "Unable to get browser extension name: $($Global:Error[0])"
        return $null
    }

    $jsonData = @"
    {
      "Google Chrome": {
        "RegistryKey": "HKLM:\\SOFTWARE\\Policies\\Google\\Chrome\\ExtensionInstallForcelist",
        "InstallSource": "https://clients2.google.com/service/update2/crx"
      },
      "Microsoft Edge": {
        "RegistryKey": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge\\ExtensionInstallForcelist",
        "InstallSource": "https://edge.microsoft.com/extensionwebstorebase/v1/crx"
      }
    }
"@


    $data = $jsonData | ConvertFrom-Json
    $browserExtensionConfig = $data.$BrowserName
    if (!$browserExtensionConfig) {
        Write-Log -LogType ERROR "Invalid browser name."
        return $null 
    }

    $registryKey = $browserExtensionConfig.RegistryKey
    $installSource = $browserExtensionConfig.InstallSource
    $newBrowserExtensionRegistryValueData = $ExtensionID + ";$installSource"

    if (!(Test-Path -Path $registryKey)) {
        if (!(Add-RegistryKey -RegistryKey $registryKey)) {
            return $null
        }
    }

    $isExtensionAlreadyAdded = $false
    (Get-Item $registryKey).Property | ForEach-Object {
        $existingBrowserExtension = (Read-RegistryValueData -RegistryKey $registryKey -ValueName $_).Value
        if ($existingBrowserExtension -eq $newBrowserExtensionRegistryValueData) {
            Write-Log -LogType INFO -Message "Browser extension $extensionName is already added in $BrowserName."
            $isExtensionAlreadyAdded = $true
            break
        }
    }

    if ($isExtensionAlreadyAdded) {
        return $null
    }
    
    # Gets the last number in the ExtensionInstallForcelist and increments it by 1 to accomodate new extension
    $latestExtensionCount = (Get-Item $registryKey).Property | Sort-Object { [int]$_ } | Select-Object -Last 1
    $newExtensionCount = [int]$latestExtensionCount + 1
    
    $registryValueObj = [PSCustomObject]@{
        ValueName = $newExtensionCount
        ValueData = $newBrowserExtensionRegistryValueData
        ValueType = 'STRING'
    }

    Write-Log -LogType INFO -Message "Adding browser extesion $extensionName to $browserName."
    
    # Creates registry value data
    Write-Log -LogType INFO -Message "Current key: $registryKey"
    Add-RegistryValue -RegistryKey $registryKey -RegistryValueObj $registryValueObj -CreateKeyIfMissing $true | Out-Null
}

Function Get-RedirectedUrl {
    Param (
        #[Parameter(ValueFromPipeline = $true)]
        [Parameter(Mandatory = $true)]
        [String]$URL,
        [Int]$Counter
    )

    if ($Counter -eq 0) {
        return $URL
    }

    $request = [System.Net.WebRequest]::Create($url)
    $request.AllowAutoRedirect = $false
    $response = $request.GetResponse()

    If ($response.StatusCode -eq "Found") {
        $URL = $response.GetResponseHeader("Location")
        $Counter = $Counter - 1
        Get-RedirectedURL -URL $URL -Counter $Counter
    }

}

Export-ModuleMember -Function 'Test-WebRequest'
Export-ModuleMember -Function 'Get-Installer'
Export-ModuleMember -Function 'Add-InstallerFolder'
Export-ModuleMember -Function 'Remove-InstallerFolder'
Export-ModuleMember -Function 'Get-InstalledProgram'
Export-ModuleMember -Function 'Get-MultipleInstalledProgram'
Export-ModuleMember -Function 'Get-InstalledService'
Export-ModuleMember -Function 'Install-Program'
Export-ModuleMember -Function 'Confirm-ProgramInstallation'
Export-ModuleMember -Function 'Confirm-ServiceInstallation'
Export-ModuleMember -Function 'Confirm-Update'
Export-ModuleMember -Function 'Set-RegistryItem'
Export-ModuleMember -Function 'Get-ProgramArchitecture'
Export-ModuleMember -Function 'Send-Keys'
Export-ModuleMember -Function 'Invoke-ModuleForUpdate'
Export-ModuleMember -Function 'Get-DownloadLink'
Export-ModuleMember -Function 'Get-DownloadLinkV2'
Export-ModuleMember -Function 'Get-LatestVersionNumber'
Export-ModuleMember -Function 'Confirm-InstallerDigitalSignature'
Export-ModuleMember -Function 'Confirm-InstallerHash'
Export-ModuleMember -Function 'Set-AgentRefresh'
Export-ModuleMember -Function 'Confirm-LogFolder'
Export-ModuleMember -Function 'Write-Log'
Export-ModuleMember -Function 'Disable-IEFirstRunCustomization'
Export-ModuleMember -Function 'Set-Alert'
Export-ModuleMember -Function 'Get-UninstallCommand'
Export-ModuleMember -Function 'Uninstall-Program'
Export-ModuleMember -Function 'Get-ProgramRegistryDisplayRegex'
Export-ModuleMember -Function 'Get-InstallCommand'
Export-ModuleMember -Function 'Approve-SelectedProgram'
Export-ModuleMember -Function 'Convert-RegistryPathToShortForm'
Export-ModuleMember -Function 'Find-RootKeyFromPath'
Export-ModuleMember -Function 'Assert-RootKeyPSDrive'
Export-ModuleMember -Function 'Read-RegistryValueData'
Export-ModuleMember -Function 'Add-RegistryKey'
Export-ModuleMember -Function 'Add-RegistryValue'
Export-ModuleMember -Function 'Remove-RegistryValue'
Export-ModuleMember -Function 'Remove-RegistryKey'
Export-ModuleMember -Function 'Get-PreInstallScriptBlock'
Export-ModuleMember -Function 'Get-PostInstallScriptBlock'
Export-ModuleMember -Function 'Get-InstallationsArchitecture'
Export-ModuleMember -Function 'Get-ProcessToTerminate'
Export-ModuleMember -Function 'Approve-Installation'
Export-ModuleMember -Function 'Get-ArchitectureConflicts'
Export-ModuleMember -Function 'Uninstall-ArchitectureConflicts'
Export-ModuleMember -Function 'Get-IgnoreExitCodes'
Export-ModuleMember -Function 'Get-M365SupportedVersion'
Export-ModuleMember -Function 'Get-M365UpdateChannel'
Export-ModuleMember -Function 'Get-PreferredArchitecture'
Export-ModuleMember -Function 'Resolve-ArchitectureSelection'
Export-ModuleMember -Function 'Find-Path'
Export-ModuleMember -Function 'Get-LowerVersionConflicts'
Export-ModuleMember -Function 'Uninstall-LowerVersionConflicts'
Export-ModuleMember -Function 'Get-InstallerFileName'
Export-ModuleMember -Function 'Approve-ProgramInstall'
Export-ModuleMember -Function 'Approve-ProgramUpdate'
Export-ModuleMember -Function 'Show-ProgramQuickInfo'
Export-ModuleMember -Function 'Add-BrowserExtension'
Export-ModuleMember -Function 'Resolve-BrowserExtensionID'
Export-ModuleMember -Function 'Get-BrowserExtensionID'
Export-ModuleMember -Function 'Get-RedirectedUrl'
Export-ModuleMember -Function 'Get-InstallerMetaData'
Export-ModuleMember -Function 'Get-VersionMatchRegex'
Export-ModuleMember -Function 'Add-UploadDataFile'
Export-ModuleMember -Function 'Get-GUID'
# SIG # Begin signature block
# MIIm1AYJKoZIhvcNAQcCoIImxTCCJsECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUfp0FCVpxUfQnOD6Tqcwbeayp
# rlCggh/uMIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B
# AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh
# MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw
# MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n
# IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE
# JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7
# fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr
# YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH
# qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv
# 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J
# mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P
# OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy
# bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe
# Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc
# uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id
# FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY
# MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw
# IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE
# DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud
# HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
# YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa
# mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+
# BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8
# ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx
# 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo
# XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p
# 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG
# 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk
# MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw
# HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp
# YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
# igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD
# eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk
# 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw
# iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ
# 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX
# nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t
# AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr
# n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn
# 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR
# XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD
# AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD
# VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw
# Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS
# NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj
# BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM
# BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK
# ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk
# vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d
# MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF
# kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa
# PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b
# kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew
# oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW
# SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg
# 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU
# KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGcDCCBNig
# AwIBAgIQJZFHyR1MEgeBUQTy/lFVfzANBgkqhkiG9w0BAQwFADBUMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgUjM2MB4XDTIyMDIxMTAwMDAwMFoXDTI1
# MDIxMDIzNTk1OVowdDELMAkGA1UEBhMCTloxETAPBgNVBAgMCEF1Y2tsYW5kMSgw
# JgYDVQQKDB9OZXcgWmVhbGFuZCBDb21wdXRpbmcgU29sdXRpb25zMSgwJgYDVQQD
# DB9OZXcgWmVhbGFuZCBDb21wdXRpbmcgU29sdXRpb25zMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEApuTd6Uz3OxE8f/hCogVVp3IIeXKnSsFh1Cgl405s
# OSmdQwP355qF7QrOurXESy9yLTbE/0k+9Kwtgn/QtrmBoZ3owIERgiGBokzmZejt
# aWHpa5vYJZxqj9Ash009EFO3VWUqtjtnNah17v0/ZHTzXXWIcn2Zl6jWxMQVriXw
# GHOXcQP7SCR6jws1k6S8sjdQb1ekJG4JzVf6zUPhzNbrYDAt+G6mBNclhdPai0Un
# Vv5gq2FfDOLyTDfsUyVv+peyA98J+L/c/geOEFMZ33eDvJZnhEhfgvayKKS0WyxU
# kPOapEwkovbLexyecIAwvk/LEhTkExCTGxZLGuGRmG3WsgpWe8nIhp/93zGLdYTH
# 7q091GgT7YIMPRnREJ8fcQn0Z9iYSrOXzi06HWT49J/j136RDkzH/ktaPpZLa0fN
# faWV0rYaPRlpoI2+8lNmh+9dbmvdL7InTyWaUZQHOcgX8mA6ZjRQKfAmC2FyJEX7
# E9MGforvwailnxTggPStFZdqCy0Svkr41jvD1XLFi0fAP57SCKJFppjDQd+1IqM8
# P8cJ37mlvgNaVrKvytns3cXi14L9uOJSRQPvGB74fwaQJNREEtN1egSqefjTam5u
# NYs2+o9YLE3dupmZ68ZiboUBrbEa94SzI9XJrvgov9/zSngPrfz8lb9RHFCpazTk
# 2KkCAwEAAaOCAZwwggGYMB8GA1UdIwQYMBaAFA8qyyCHKLjsb0iuK1SmKaoXpM0M
# MB0GA1UdDgQWBBQsTCOGcgbKnn97jUc5qXMhD2f33zAOBgNVHQ8BAf8EBAMCB4Aw
# DAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAzARBglghkgBhvhCAQEE
# BAMCBBAwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcCARYX
# aHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAwPqA8
# oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWdu
# aW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0cDov
# L2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5j
# cnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3
# DQEBDAUAA4IBgQCM8pBYRTJl1NljQjtWjO+bI1M9/73uX+Usw2Prq0dBDtLD1VKt
# wP/ubq8jI4rIAJRdCv5Qx7GJfHyX7AlUciYmEUYgLxHb1l6M4FE0v/kKkntJ/Ywv
# b4QzNgw/ROA6poTwizxkrR0/KRB6dIiUE91IhLddbxOXm7/CGLxTf9sRb0z8t7hT
# uBrIUkbrV+4D7wsSVXnd5fkrSx6cexhGrC57ZLy08gW9gZmFa3B9H8K8Ep+giTLJ
# gBmIfL+80uIodiOsWG2RuCeCiiilfEZRw1C0xklSzKfpvGkZ4tgFycHJPom+o6rZ
# aRtoQmlFSdFpGI6nT+Zrn2FrQhELjA7QbWL8Vp6VUtOQY1otb3Jof5FydMHjf/iL
# y3Y75wnkBemti2at03bf/PUSjdXLALIgi/HXLqKScNQayiWdTadJvon1PLERInlB
# ZA7kg8eUTLp0T6CWcH5I8gm6ycJ24gVAzPTH3KSSZMnyNCM1T1/Rc3g2KMyqGotN
# fuhDIe9vO2Q0YUwwggbsMIIE1KADAgECAhAwD2+s3WaYdHypRjaneC25MA0GCSqG
# SIb3DQEBDAUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEU
# MBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0
# d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhv
# cml0eTAeFw0xOTA1MDIwMDAwMDBaFw0zODAxMTgyMzU5NTlaMH0xCzAJBgNVBAYT
# AkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZv
# cmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDElMCMGA1UEAxMcU2VjdGlnbyBS
# U0EgVGltZSBTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAMgbAa/ZLH6ImX0BmD8gkL2cgCFUk7nPoD5T77NawHbWGgSlzkeDtevEzEk0
# y/NFZbn5p2QWJgn71TJSeS7JY8ITm7aGPwEFkmZvIavVcRB5h/RGKs3EWsnb111J
# TXJWD9zJ41OYOioe/M5YSdO/8zm7uaQjQqzQFcN/nqJc1zjxFrJw06PE37PFcqwu
# Cnf8DZRSt/wflXMkPQEovA8NT7ORAY5unSd1VdEXOzQhe5cBlK9/gM/REQpXhMl/
# VuC9RpyCvpSdv7QgsGB+uE31DT/b0OqFjIpWcdEtlEzIjDzTFKKcvSb/01Mgx2Bp
# m1gKVPQF5/0xrPnIhRfHuCkZpCkvRuPd25Ffnz82Pg4wZytGtzWvlr7aTGDMqLuf
# DRTUGMQwmHSCIc9iVrUhcxIe/arKCFiHd6QV6xlV/9A5VC0m7kUaOm/N14Tw1/Ao
# xU9kgwLU++Le8bwCKPRt2ieKBtKWh97oaw7wW33pdmmTIBxKlyx3GSuTlZicl57r
# jsF4VsZEJd8GEpoGLZ8DXv2DolNnyrH6jaFkyYiSWcuoRsDJ8qb/fVfbEnb6ikEk
# 1Bv8cqUUotStQxykSYtBORQDHin6G6UirqXDTYLQjdprt9v3GEBXc/Bxo/tKfUU2
# wfeNgvq5yQ1TgH36tjlYMu9vGFCJ10+dM70atZ2h3pVBeqeDAgMBAAGjggFaMIIB
# VjAfBgNVHSMEGDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUGqH4
# YRkgD8NBd0UojtE1XwYSBFUwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwEQYDVR0gBAowCDAGBgRVHSAAMFAG
# A1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VU0VSVHJ1
# c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2BggrBgEFBQcBAQRqMGgw
# PwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNvbS9VU0VSVHJ1c3RS
# U0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRy
# dXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAbVSBpTNdFuG1U4GRdd8DejILLSWE
# EbKw2yp9KgX1vDsn9FqguUlZkClsYcu1UNviffmfAO9Aw63T4uRW+VhBz/FC5RB9
# /7B0H4/GXAn5M17qoBwmWFzztBEP1dXD4rzVWHi/SHbhRGdtj7BDEA+N5Pk4Yr8T
# AcWFo0zFzLJTMJWk1vSWVgi4zVx/AZa+clJqO0I3fBZ4OZOTlJux3LJtQW1nzclv
# kD1/RXLBGyPWwlWEZuSzxWYG9vPWS16toytCiiGS/qhvWiVwYoFzY16gu9jc10rT
# Pa+DBjgSHSSHLeT8AtY+dwS8BDa153fLnC6NIxi5o8JHHfBd1qFzVwVomqfJN2Ud
# vuq82EKDQwWli6YJ/9GhlKZOqj0J9QVst9JkWtgqIsJLnfE5XkzeSD2bNJaaCV+O
# /fexUpHOP4n2HKG1qXUfcb9bQ11lPVCBbqvw0NP8srMftpmWJvQ8eYtcZMzN7iea
# 5aDADHKHwW5NWtMe6vBE5jJvHOsXTpTDeGUgOw9Bqh/poUGd/rG4oGUqNODeqPk8
# 5sEwu8CgYyz8XBYAqNDEf+oRnR4GxqZtMl20OAkrSQeq/eww2vGnL8+3/frQo4TZ
# J577AWZ3uVYQ4SBuxq6x+ba6yDVdM3aO8XwgDCp3rrWiAoa6Ke60WgCxjKvj+QrJ
# VF3UuWp0nr1Irpgwggb1MIIE3aADAgECAhA5TCXhfKBtJ6hl4jvZHSLUMA0GCSqG
# SIb3DQEBDAUAMH0xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNo
# ZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRl
# ZDElMCMGA1UEAxMcU2VjdGlnbyBSU0EgVGltZSBTdGFtcGluZyBDQTAeFw0yMzA1
# MDMwMDAwMDBaFw0zNDA4MDIyMzU5NTlaMGoxCzAJBgNVBAYTAkdCMRMwEQYDVQQI
# EwpNYW5jaGVzdGVyMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLDAqBgNVBAMM
# I1NlY3RpZ28gUlNBIFRpbWUgU3RhbXBpbmcgU2lnbmVyICM0MIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEApJMoUkvPJ4d2pCkcmTjA5w7U0RzsaMsBZOSK
# zXewcWWCvJ/8i7u7lZj7JRGOWogJZhEUWLK6Ilvm9jLxXS3AeqIO4OBWZO2h5YEg
# ciBkQWzHwwj6831d7yGawn7XLMO6EZge/NMgCEKzX79/iFgyqzCz2Ix6lkoZE1ys
# /Oer6RwWLrCwOJVKz4VQq2cDJaG7OOkPb6lampEoEzW5H/M94STIa7GZ6A3vu03l
# PYxUA5HQ/C3PVTM4egkcB9Ei4GOGp7790oNzEhSbmkwJRr00vOFLUHty4Fv9Gbsf
# PGoZe267LUQqvjxMzKyKBJPGV4agczYrgZf6G5t+iIfYUnmJ/m53N9e7UJ/6GCVP
# E/JefKmxIFopq6NCh3fg9EwCSN1YpVOmo6DtGZZlFSnF7TMwJeaWg4Ga9mBmkFgH
# gM1Cdaz7tJHQxd0BQGq2qBDu9o16t551r9OlSxihDJ9XsF4lR5F0zXUS0Zxv5F4N
# m+x1Ju7+0/WSL1KF6NpEUSqizADKh2ZDoxsA76K1lp1irScL8htKycOUQjeIIISo
# h67DuiNye/hU7/hrJ7CF9adDhdgrOXTbWncC0aT69c2cPcwfrlHQe2zYHS0RQlNx
# dMLlNaotUhLZJc/w09CRQxLXMn2YbON3Qcj/HyRU726txj5Ve/Fchzpk8WBLBU/v
# uS/sCRMCAwEAAaOCAYIwggF+MB8GA1UdIwQYMBaAFBqh+GEZIA/DQXdFKI7RNV8G
# EgRVMB0GA1UdDgQWBBQDDzHIkSqTvWPz0V1NpDQP0pUBGDAOBgNVHQ8BAf8EBAMC
# BsAwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDBKBgNVHSAE
# QzBBMDUGDCsGAQQBsjEBAgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3Rp
# Z28uY29tL0NQUzAIBgZngQwBBAIwRAYDVR0fBD0wOzA5oDegNYYzaHR0cDovL2Ny
# bC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNBVGltZVN0YW1waW5nQ0EuY3JsMHQGCCsG
# AQUFBwEBBGgwZjA/BggrBgEFBQcwAoYzaHR0cDovL2NydC5zZWN0aWdvLmNvbS9T
# ZWN0aWdvUlNBVGltZVN0YW1waW5nQ0EuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8v
# b2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEATJtlWPrgec/vFcMy
# bd4zket3WOLrvctKPHXefpRtwyLHBJXfZWlhEwz2DJ71iSBewYfHAyTKx6XwJt/4
# +DFlDeDrbVFXpoyEUghGHCrC3vLaikXzvvf2LsR+7fjtaL96VkjpYeWaOXe8vrqR
# ZIh1/12FFjQn0inL/+0t2v++kwzsbaINzMPxbr0hkRojAFKtl9RieCqEeajXPawh
# j3DDJHk6l/ENo6NbU9irALpY+zWAT18ocWwZXsKDcpCu4MbY8pn76rSSZXwHfDVE
# Ha1YGGti+95sxAqpbNMhRnDcL411TCPCQdB6ljvDS93NkiZ0dlw3oJoknk5fTtOP
# D+UTT1lEZUtDZM9I+GdnuU2/zA2xOjDQoT1IrXpl5Ozf4AHwsypKOazBpPmpfTXQ
# MkCgsRkqGCGyyH0FcRpLJzaq4Jgcg3Xnx35LhEPNQ/uQl3YqEqxAwXBbmQpA+oBt
# lGF7yG65yGdnJFxQjQEg3gf3AdT4LhHNnYPl+MolHEQ9J+WwhkcqCxuEdn17aE+N
# t/cTtO2gLe5zD9kQup2ZLHzXdR+PEMSU5n4k5ZVKiIwn1oVmHfmuZHaR6Ej+yFUK
# 7SnDH944psAU+zI9+KmDYjbIw74Ahxyr+kpCHIkD3PVcfHDZXXhO7p9eIOYJanwr
# CKNI9RX8BE/fzSEceuX1jhrUuUAxggZQMIIGTAIBATBoMFQxCzAJBgNVBAYTAkdC
# MRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVi
# bGljIENvZGUgU2lnbmluZyBDQSBSMzYCECWRR8kdTBIHgVEE8v5RVX8wCQYFKw4D
# AhoFAKBwMBAGCisGAQQBgjcCAQwxAjAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEW
# BBTy5xwlBDiHFz4llAp11Aa3OmMX6jANBgkqhkiG9w0BAQEFAASCAgCAr4XMz052
# zQVCqeiq5Sq3c4rJcPkhHpaXaeBR443TgRSO3RgEZugFoCNaEE6Qgbv0PE10uJPx
# zrEv/ho/OIFoSV0HrM2V55RbGClmJSDECWxDMCLsHjrYe/fk6m1gjLc3vukzIwye
# yzcfNujl2OLU2DJPj5xHefpzjrd7CfhvqB0QJtNyQAuEeP84M/UlXxto9541LJyU
# fqMoux4+8FGwr4uGOoGAV9/q5deLkj3vcZ9iST/a8ZS0NtF/mptWvfaPUIWLp0/I
# W7jlAABbc97PAPlYuqLMjF+KQmbnVuEEayN4egPul0cUB3Ss3NdBs52/0EekPIz7
# GiM3GW4SwX+iZzBCakmhKkQkUA3gJlhwJD4T2L/ld+XNt0w2IqwtxeAok0en1bmV
# 1Yc9T44ay5baM2p6W4oEYtuH3ee6kKso6bKLp/8gDJGlBFjHNKYL07/FPbh1xSxn
# XDfeP2O4vXk+VsHbcjsyHCUUA+Fn8StA08HiZVzMqA2bxoikZdW0+lxC3Xtt+UQc
# Wdn9u6YpU9nn1mZ2MTG4jDO7DKDLpz/N10AaAw796hRyHvU9MFKxJKeyIuKEx1Cy
# iSf9QWBa3ai9r2ZuRLYM5l3Eko7NxxjOhJwnC9eihlNrXWjUCEWOJQKY7V8bIWx9
# p0SLt9kA/mF8EF5vTS8BMKnEj1y+Sg181qGCA0swggNHBgkqhkiG9w0BCQYxggM4
# MIIDNAIBATCBkTB9MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5j
# aGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0
# ZWQxJTAjBgNVBAMTHFNlY3RpZ28gUlNBIFRpbWUgU3RhbXBpbmcgQ0ECEDlMJeF8
# oG0nqGXiO9kdItQwDQYJYIZIAWUDBAICBQCgeTAYBgkqhkiG9w0BCQMxCwYJKoZI
# hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA0MDQyMDQ1NTlaMD8GCSqGSIb3DQEJ
# BDEyBDDqScAzixvQPDBfGr2kcZprZBeZROYKfRMKqnDV5uk5jmxG++eoNwQMv81V
# qQdsbk4wDQYJKoZIhvcNAQEBBQAEggIAVmYca6K0gA8AZGuXhc7Zi4S1T1fyWr1D
# BvDKz37f2NKx+up/+AhVuCVufPQVOuAzllpz23J3zhWBNoNaCdRANMTgZnTYRp4r
# wECXW+ytEFSTy4jWtU14d/K3i1ugerbhMe/K3KKhoOG9IxvKMp9NX82eG+V0oj1P
# lVk3D0Af5JG0AqJSKOtpPt9JcftrQveI1a2T9g9ZRip3rj9AYTSfjk/ZvMRpCgUF
# 94F3vdEwSpDHzyZJgMSVRN33GCrP59RR5KI9Vd99skOf4bmcacu82AtKE7d6cruP
# 7wzKVXXUL+eAlTAFuP+04+3QpUQ3vBuAyagGo0NBEWp76pXuotKE4EzoctKQXwmK
# A6LxUzOk9dKmngMIIPfGRCKa9jXbZt0XEteek4OwNfvwg/0BNmn6gLd5b1JN5tgZ
# XX343mR952oNoTY4jhsI+OXwFcdBeOWQIwZIeC4zW/cjHYDR+oYi2uAFH4/UlaJs
# 1bbVOOGyLF45+2/0Bt3Y4ZZEnsHldHrSoXTNpfcdulMl8StGVIAXu/nueO61toJm
# SPGCMF7aS1i+f0rLx3TzTm9ywc8l0/M0m+PmNW+MIKT+uffPq0m4xDFs4mNK0FKn
# +9UiTc2GCQyu6/3WhnbrrAjM72qB5eICGAZUyoxugmBN+AApQFoVeCococZ1abIV
# wV/Q23wjznU=
# SIG # End signature block