AppManiProgramManager.psm1
# 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 Update 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: $($Error[0])" return $null } } # catches other exceptions catch { Write-Log -LogType ERROR -Message "Failed to execute test webrequest: $($Error[0])" return $null } } return $webrequest } # Downloads installer Function Get-Installer { Param ( $DownloadLink, $SavePath ) $ProgressPreference = 'SilentlyContinue' # Tests if save path is existing if (Test-Path $savePath) { $attempts = 1 $maxAttempts = 3 $null = $fileName # Downloads the file while ($attempts -le $maxAttempts) { Write-Log -LogType INFO -Message "Attempts: $attempts" # Downloads the file try { $download = Invoke-WebRequest -Uri $downloadLink } catch [System.NotSupportedException] { Disable-IEFirstRunCustomization } catch { Write-Log -LogType ERROR -Message "Unable to download installer: $($Error[0])" return $null } # Tries to get filename from Content-Disposition header if ($download.Headers["Content-Disposition"]) { try { $content = [System.Net.Mime.ContentDisposition]::new($download.Headers["Content-Disposition"]) $fileName = $content.FileName } catch { } } # If not, get capture filename from download link if (!$fileName) { $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]) { $fileName = $matches[0] } # If that still doesn't work use fallback filename else { $fileName = "$global:programNameNoSpace.exe" } } # Actually saves the file to disk $installerPath = $SavePath + '\' + $fileName #Write-Host $SavePath $f = [IO.File]::OpenWrite($installerPath); try { $download.RawContentStream.WriteTo($f); } finally { $f.Dispose(); } if (Test-Path $installerPath) { return $installerPath } $attempts++ } } else { Write-Log -LogType ERROR -Message "Download path $SavePath not existing. Please specify a valid path." } 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: $($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: $($Error[0])" return $null } } #Deletes installer Function Remove-InstallerFolder { Param ( $Path, $CleanupDelay ) $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 INFO -Message "Clean up will be performed in $cleanupDelay seconds." Start-Sleep -Seconds $CleanupDelay Write-Log -LogType INFO -Message "Cleaning up..." -NoNewline if ((Get-Location).Path -eq $Path) { Set-Location .. } # Removes a file try { Remove-Item -Path $Path -Recurse -Force } catch { Write-Log -LogType ERROR -Message "Failed to delete installer folder: $($Error[0])" return $null } Write-Log -LogType INFO -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 ) $HKUPSDrive = Get-PSDrive HKU -ErrorAction SilentlyContinue if (!($HKUPSDrive)) { New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -Scope Script | Out-Null } #Write-Host $RegistryDisplayName $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\*" $installedProgram = $Apps | Where-Object DisplayName -match $RegistryDisplayName if ($installedProgram) { if ($installedProgram.Length -gt 1) { Write-Log -LogType WARNING -Message "Search returned $($installedProgram.Length) hits. This function only will return the topmost result." } return $installedProgram[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 } #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 INFO -Message "Executing command $installCommand" try { cmd /c $installCommand } catch { Write-Log -LogType ERROR -Message "Unable to install: $($Error[0])" return 1 } Write-Log -LogType INFO -Message "Execution completed with exit code $LASTEXITCODE" return $LASTEXITCODE } Function Confirm-ProgramInstallation { Param ( $RegistryDisplayName ) # Loops X number of times to check registry keys for the program $tries = 0 while ($tries -le 3) { $tries++ Write-Log -LogType INFO -Message "Verifying installation. Tries: $tries" $installedProgram = Get-InstalledProgram -RegistryDisplayName $RegistryDisplayName if ($null -ne $installedProgram) { return $installedProgram } 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 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 INFO -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 } # 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, $VersionMatchRegex ) # 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 (!$latestVersion) { return $null } Write-Log -LogType INFO -Message "Latest version available: $latestVersion" # 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 (!($latestVersion -match $VersionMatchRegex)) { Write-Log -LogType ERROR -Message "Latest version doesn't match version match regex." return $false } else { $latestVersion = $matches[0] } try { $isProgramForUpdate = [version]$InstalledVersion -lt [version]$latestVersion } catch [System.Management.Automation.RuntimeException] { $isProgramForUpdate = $InstalledVersion -lt $latestVersion } 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 ( $Program ) if ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall') { return "x86" } elseif ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') { return "x64" } else { 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: $($Error[0])" return $null } } Else { Write-Log -LogType INFO -Message "Module $ModuleName is already up to date." } } } } Function Get-DownloadLink { Param ( $ProgramName, $Architecture ) $Architecture = if ($Architecture) { $Architecture } else { 'x64' } $attempts = 1 $maxAttempts = 3 while ($attempts -le $maxAttempts) { Write-Log -LogType INFO -Message "Attempts: $attempts" try { switch ($ProgramName) { '7-zip' { $HomePage = "https://www.7-zip.org/" if ($architecture -eq 'x64') { $architecture = '-x64' } else { $architecture = '' } $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html' $Pattern = '<A href=\"(?<link>a/7z\d+{0}\.exe)\">Download</A>' -f $architecture $AllMatches = ([regex]$Pattern).Matches($HTML) $link = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value if ($null -ne $link) { $downloadLink = $HomePage + "$($link)" return $downloadLink } else { Write-Log -LogType ERROR -Message "Version requested is not available." return $null } } 'Adobe Acrobat' { # $versionOverride = '22.003.20282' 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 INFO -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 return $downloadLink } 'Audacity' { if ($architecture -eq 'x64') { $architecture = '64 bit' } elseif ($architecture -eq 'x86') { $architecture = '32 bit' } $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/' $Pattern = '<a href=\"(?<link>.*)\">Audacity .+? {0} installer</a>' -f $architecture $AllMatches = ([regex]$Pattern).Matches($HTML) $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value return $downloadLink } 'Bitwarden' { $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows' $downloadLink = (Invoke-WebRequest -Uri $originalLink -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location return $downloadLink } '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 return $downloadLink } 'CutePDF Writer' { $downloadLink = 'https://www.cutepdf.com/download/CuteWriter.exe' return $downloadLink } '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 return $downloadLink } 'Dropbox' { $originalLink = 'https://www.dropbox.com/download?full=1&plat=win' $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 return $downloadLink } 'Filezilla' { if ($architecture -eq 'x64') { $architecture = 'win64' } elseif ($architecture -eq 'x86') { $architecture = '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 $architecture $AllMatches = ([regex]$Pattern).Matches($HTML) $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value return $downloadLink } '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 return $downloadLink } 'Google Drive' { $downloadLink = 'https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe' return $downloadLink } 'GPL Ghostscript' { if ($Architecture -eq 'x64') { $Architecture = 'w64' } else { $Architecture = '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])$Architecture.exe" return $downloadLink } 'Google Chrome' { $downloadLink = 'http://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi' return $downloadLink } 'HP Support Assistant' { $downloadLink = 'https://ftp.ext.hp.com/pub/softpaq/sp141501-142000/sp141886.exe' return $downloadLink } 'IrfanView' { $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 if ($Architecture -eq 'x86') { $Architecture = '' } 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 $architecture) }))[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 $ErrorType = $Response.error if ($ErrorType -ne $Null) { throw "ERROR RETURNED $ErrorType" return $Null } return ($info.data)[0].url } catch { Write-Error $_ } } 'Jabra Direct' { $downloadLink = 'https://jabraxpressonlineprdstor.blob.core.windows.net/jdo/JabraDirectSetup.exe' return $downloadLink } 'Java 8' { if ($architecture -eq 'x64') { $architecture = '\(64-bit\)' } else { $architecture = '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 title=\"Download Java software for Windows {0}\" href=\"(?<link>.*)\"><img' -f $architecture $AllMatches = ([regex]$Pattern).Matches($HTML) $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value #return $downloadLink } '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 Installer Version \d+\" href=\"(?<link>.*)\">Word' $AllMatches = ([regex]$Pattern).Matches($HTML) $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value return $domainName + $downloadLink } '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 return $downloadLink } 'Microsoft 365' { $downloadLink = 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_15128-20224.exe' return $downloadLink } 'Mozilla Firefox' { 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 return $downloadLink } 'Net Monitor for Employees Agent' { $downloadLink = 'https://networklookout.com/dwn/nmemplpro_agent.msi' return $downloadLink } 'Notepad++' { $NPlusPlusWebsite = "https://notepad-plus-plus.org" $DownloadPage = $NPlusPlusWebsite + "/downloads" $filter = '*Installer.exe' if ($architecture -eq 'x64') { $filter = '*Installer.x64.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: $($Error[0])" return $null } if ($null -ne $currentVersion) { $NPlusPlusCurrentVersionDownloadPage = $NPlusPlusWebsite + $currentVersion try { $downloadLink = $(Invoke-WebRequest $NPlusPlusCurrentVersionDownloadPage).Links.href -like $filter | Select-Object -First 1 return $downloadLink } catch { Write-Log -LogType ERROR -Message "The script ran into an issue: $($Error[0])" return $null } } else { Write-Log -LogType ERROR -Message "Unable to find current version." return $null } } 'PDFCreator' { $originalLink = 'https://download.pdfforge.org/download/pdfcreator/PDFCreator-stable?download' $downloadLink = (Invoke-WebRequest -Uri $originalLink -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location return $downloadLink } 'Putty' { $PuttyDownloadPage = "https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html" if ($Architecture -eq 'x64') { $Architecture = 'w64' } elseif ($Architecture -eq 'x86') { $Architecture = 'w32' } try { $links = (Invoke-WebRequest $PuttyDownloadPage).Links.href } catch { Write-Log -LogType ERROR -Message "The script ran into an issue: $($Error[0])" return $null } if ($links) { $downloadLink = $links | Where-Object { ($_ -match "$Architecture/(.+?)-installer\.msi$") } } else { Write-Log -LogType ERROR -Message "No links found." return $null } if ($downloadLink) { return $downloadLink } else { Write-Log -LogType ERROR -Message "Unable to capture download link." return $null } } 'Python' { $downloadLink = $(Invoke-WebRequest -Uri 'https://www.python.org/downloads/').Links.href | Where-Object { $_ -like '*.exe' } if ($architecture -eq 'x86') { $downloadLink = $downloadLink -replace "-amd64", "" } return $downloadLink } 'Synology Drive Client' { $downloadLinkFormat = 'https://global.download.synology.com/download/Utility/SynologyDriveClient/{0}/Windows/Installer/Synology%20Drive%20Client-{0}.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 return $downloadLink } 'Sysmon64' { $downloadLink = 'https://download.sysinternals.com/files/Sysmon.zip' return $downloadLink } '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 return $downloadLink } 'TreeSize Free' { $downloadLink = 'https://downloads.jam-software.de/treesize_free/TreeSizeFreeSetup.exe' return $downloadLink } 'UniPrint' { $regex = "UniPrintClientMSI_\d+_$architecture.zip$" $downloadLink = (Invoke-WebRequest -Uri 'https://www.uniprint.net/en/uniprint-client/' -UseBasicParsing).Links.href | Where-Object { $_ -match $regex } return $downloadLink } 'VLC' { $VLCDownloadPage = "https://www.videolan.org/vlc/download-windows.html" if ($architecture -eq 'x64') { $filter = '*win64.exe' } elseif ($architecture -eq 'x86') { $filter = '*win32.exe' } try { $versionDownloadPage = $(Invoke-WebRequest -Uri $VLCDownloadPage).Links | Where-Object href -like $filter } catch { Write-Log -LogType ERROR -Message "The script ran into an issue: $($Error[0])" return $null } $versionDownloadPage = "https:" + $versionDownloadPage.href try { $downloadLink = $((Invoke-WebRequest -Uri $versionDownloadPage).Links | Where-Object href -like $filter | Select-Object -First 1).href return $downloadLink } catch { Write-Log -LogType ERROR -Message "The script ran into an issue: $($Error[0])" return $null } } 'Wireshark' { $Release = 'Stable Release' $wiresharkDownloadPage = "https://www.wireshark.org/download.html" $versionRegex = '\((.+?)\)' if ($Architecture -eq 'x64') { $Architecture = 'win64' } elseif ($Architecture -eq 'x86') { $Architecture = 'win32' } $webRequest = Invoke-WebRequest -Uri $WiresharkDownloadPage # Checks webrequest links for the version number e.g. "Stable 3.6.6" then finds the download link for it ($webRequest).Links.innerHTML | Foreach-Object { if ($_ -match "^$release") { $_ -match $versionRegex | Out-Null $version = $matches[1] } } $downloadLink = $webRequest.Links.href | Where-Object { $_ -match "$Architecture-$version" } return $downloadLink } 'Zoom' { $downloadLink = 'http://zoom.us/client/latest/ZoomInstallerFull.msi' return $downloadLink } Default { Write-Log -LogType INFO -Message "No matching function to retrieve download link for $programName" return $null } } } catch [System.NotSupportedException] { Disable-IEFirstRunCustomization } catch { Write-Log -LogType ERROR "Unable to retrieve download link for $ProgramName. $($Error[0])" } if ($downloadLink) { return $downloadLink } $attempts++ #Start-Sleep 5 } 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 ) try { switch ($ProgramName) { '7-zip' { $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html' -ErrorAction Stop $Pattern = '<B>Download 7-Zip (?<version>.*) \((.+?)\)</B>' $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value 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 -ErrorAction Stop $Pattern = '<title>(?<version>[\d\.]+) ' $AllMatches = ([regex]$Pattern).Matches($latestVersionHTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value break } 'Audacity' { $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/' -ErrorAction Stop $Pattern = '<h2>Current Version: (?<version>.*)</h2>' $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value 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/windows/workspace-app-for-windows-latest.html' -ErrorAction Stop $Pattern = '<p><b>Version:</b> (?<version>[\d\.]+).*</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>' $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 = '<p><h3>Version (?<version>.*)</h3></p>' $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value break } '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 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' { $HTML = Invoke-RestMethod 'https://chromereleases.googleblog.com/search/label/Desktop%20Update' -ErrorAction Stop $Pattern = 'The Stable channel has been updated to (?<version>[\d\.]+) for' $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value 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' { $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 Installer Version (?<version>[\d\.]+)\"' $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($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 $break } 'Network Lookout for Employees Pro' { $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 } '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 } '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' { $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 } '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 } 'Sysmon' { $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 } '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 } 'Wireshark' { $HTML = Invoke-RestMethod 'https://www.wireshark.org/download.html' -ErrorAction Stop $Pattern = "<a.*>Stable Release \((?<version>.*)\)</a>" $AllMatches = ([regex]$Pattern).Matches($HTML) $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value 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 break } default { $latestVersion = $null } } } catch [System.NotSupportedException] { Disable-IEFirstRunCustomization } catch { Write-Log -LogType ERROR "Unable to retrieve latest version number for $ProgramName. $($Error[0])" return $null } $obj = [PSCustomObject]@{ ProgramName = $ProgramName VersionNumber = $latestVersion } return $obj } Function Confirm-InstallerValidity { Param( $FilePath ) $varChain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain try { $verification = $varChain.Build((Get-AuthenticodeSignature -FilePath "$FilePath").SignerCertificate) return $verification } catch [System.Management.Automation.MethodInvocationException] { $err = ( "'$FilePath' 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 } } Function Set-AgentRefresh { Param ( $NewRefreshCheckValue ) $auditRefreshRegistryPath = 'HKLM:\SOFTWARE\NZCS\ServiceCAT' $auditRefreshRegistryItemName = 'RefreshCheck' # Gets refresh check value $CurrentRefreshCheckValue = Get-ItemProperty -Path $auditRefreshRegistryPath -Name $auditRefreshRegistryItemName #| Out-Null# -ErrorAction Stop # Checks if new and current RefreshCheck values are different if ($NewRefreshCheckValue -ne $CurrentRefreshCheckValue.RefreshCheck) { # Sets new RefreshCheck value if they are different Set-RegistryItem -RegistryPath $auditRefreshRegistryPath -Name $auditRefreshRegistryItemName -Value $NewRefreshCheckValue | 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: $($Error[0])" return $false } } Write-Log -LogType INFO -Message "Logs will be saved at $logFolderPath." return $true } Function Write-Log { Param ( $LogType, $Message ) $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 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: $($Error[0])" return $null } } Function Set-Alert { Param ( $AlertsFolderPath, [int]$Category = 1999, $Subject, $Body, [int]$Priority = 3, $AutoComplete ) Function Get-GUID($maxSize = 10) { $g = [guid]::NewGuid() $v = [string]$g $v = $v.Replace("-", "") return $v.substring(0, $maxSize) } $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-Host "Alert folder created." } catch { Write-Log -LogType ERROR -Message "Failed to create alerts folder: $($Error[0])" return $null } } $newAlertFilePath = $AlertsFolderPath + '\alert_' + $(Get-GUID) + '.json' try { $newAlert | ConvertTo-Json | 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: $($Error[0].Exception)" } } Function Get-UninstallCommand { Param ( $InstalledProgram ) # Extracts the path from the uninstall command Function Find-Path { Param ( $Command ) $matches = @() $pathRegex = '[a-zA-Z]:\\(((?![<>:"\/\\|?*]).)+((?<![ .])\\)?)*' if (!($Command -match $pathRegex)) { Write-Log -LogType ERROR -Message "Unable to extract uninstall path." return $null } $Path = $($matches[0]).Trim() return $Path } # 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 $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 INFO -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 INFO -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 += ' /quiet' # return $uninstallCommand # } 'Citrix Workspace*' { Write-Log -LogType INFO -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 INFO -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 INFO -Message "Override found." $uninstallCommand += ' /silent' return $uninstallCommand } 'Jabra Direct*' { Write-Log -LogType INFO -Message "Override found." $App = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object DisplayName -eq $ProgramName $uninstallCommand = $App.QuietUninstallString 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 # } Default { Write-Log -LogType INFO -Message "No override found." } } # If QuietUninstallString property is present, return right away if ($InstalledProgram.QuietUninstallString) { Write-Log -LogType INFO -Message "Quiet uninstall command found from 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 INFO -Message "Msiexec uninstall command found from 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 INFO "Attempting to determine quiet uninstall command for $($InstalledProgram.DisplayName)." $uninstallExePath = Find-Path -Command $uninstallCommand $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 if (!$installerTool) { Write-Log -LogType INFO -Message "Unable to determine installer tool." } $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand switch -Wildcard ($installerTool) { '*InstallAware*' { Write-Log -LogType INFO -Message "Installer created using InstallAware." $uninstallCommand += ' /s' return $uninstallCommand } '*Inno Setup*' { Write-Log -LogType INFO -Message "Installer created using Inno Setup." $uninstallCommand += ' /VERYSILENT /NORESTART' return $uninstallCommand } '*InstallShield*' { Write-Log -LogType INFO -Message "Installer created using InstallShield." $uninstallCommand += ' -s' return $uninstallCommand } '*Nullsoft*' { Write-Log -LogType INFO -Message "Installer created using Nullsoft." $uninstallCommand += ' /S' return $uninstallCommand } '*Advanced Installer*' { Write-Log -LogType INFO -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 INFO -Message "Executing command $uninstallCommand" try { cmd /c $uninstallCommand } catch { Write-Log -LogType ERROR -Message "Unable to uninstall: $($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$' 'Notepad++' = 'Notepad\+\+' 'Python 3' = 'Python 3\.[\d\.]+ \(.+\)' } $regex = $RegistryDisplayNameRegexes.$ProgramName if (!$regex) { return $ProgramName } else { return $regex } } # 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 } 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-LatestVersionNumber' Export-ModuleMember -Function 'Confirm-InstallerValidity' 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 'Approve-SelectedProgram' |