From 09b54a6eee94adc5efd6f8cf9b5fb1a556888a3d Mon Sep 17 00:00:00 2001 From: Cryostrixx Date: Wed, 9 Oct 2024 23:43:16 -0700 Subject: [PATCH] Fix WinUtil admin elevation for stable and pre-release builds --- scripts/start.ps1 | 90 +++++++++++++++-------- windev.ps1 | 182 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 208 insertions(+), 64 deletions(-) diff --git a/scripts/start.ps1 b/scripts/start.ps1 index c289709c..fb6323e0 100644 --- a/scripts/start.ps1 +++ b/scripts/start.ps1 @@ -1,11 +1,12 @@ <# .NOTES - Author : Chris Titus @christitustech - Runspace Author: @DeveloperDurp - GitHub : https://github.com/ChrisTitusTech - Version : #{replaceme} + Author : Chris Titus @christitustech + Runspace Author : @DeveloperDurp + GitHub : https://github.com/ChrisTitusTech + Version : #{replaceme} #> +# Define the arguments for the WinUtil script param ( [switch]$Debug, [string]$Config, @@ -17,12 +18,13 @@ if ($Debug) { $DebugPreference = "Continue" } +# Handle the -Config parameter if ($Config) { $PARAM_CONFIG = $Config } -$PARAM_RUN = $false # Handle the -Run switch +$PARAM_RUN = $false if ($Run) { Write-Host "Running config file tasks..." $PARAM_RUN = $true @@ -39,38 +41,66 @@ $sync.version = "#{replaceme}" $sync.configs = @{} $sync.ProcessRunning = $false -if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Output "Winutil needs to be run as Administrator. Attempting to relaunch." - $argList = @() +# Store elevation status of the process +$isElevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - $PSBoundParameters.GetEnumerator() | ForEach-Object { - $argList += if ($_.Value -is [switch] -and $_.Value) { - "-$($_.Key)" - } elseif ($_.Value) { - "-$($_.Key) `"$($_.Value)`"" - } +# Initialize the arguments list array +$argsList = @() + +# Add the passed parameters to $argsList +$PSBoundParameters.GetEnumerator() | ForEach-Object { + $argsList += if ($_.Value -is [switch] -and $_.Value) { + "-$($_.Key)" + } elseif ($_.Value) { + "-$($_.Key) `"$($_.Value)`"" } +} - $script = if ($MyInvocation.MyCommand.Path) { - "& { & '$($MyInvocation.MyCommand.Path)' $argList }" - } else { - "iex '& { $(irm https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1) } $argList'" - } - - $powershellcmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" } - $processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $powershellcmd } - - Start-Process $processCmd -ArgumentList "$powershellcmd -ExecutionPolicy Bypass -NoProfile -Command $script" -Verb RunAs +# Set the download URL for the latest release +$downloadURL = "https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1" +# Download the WinUtil script to '$env:TEMP' +try { + Write-Host "Downloading the latest stable WinUtil version..." -ForegroundColor Green + Invoke-RestMethod $downloadURL -OutFile "$env:TEMP\winutil.ps1" +} catch { + Write-Host "Error downloading WinUtil: $_" -ForegroundColor Red break } -$dateTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" +# Setup the commands used to launch the script +$powershellCmd = if (Get-Command pwsh.exe -ErrorAction SilentlyContinue) { "pwsh.exe" } else { "powershell.exe" } +$processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $powershellCmd } +# Setup the script's launch arguments +$launchArguments = "-ExecutionPolicy Bypass -NoProfile -File `"$env:TEMP\winutil.ps1`" $argsList" +if ($processCmd -ne $powershellCmd) { + $launchArguments = "$powershellCmd $launchArguments" +} + +# Set the title of the running PowerShell instance +$BaseWindowTitle = $MyInvocation.MyCommand.Path ?? $MyInvocation.MyCommand.Definition +$Host.UI.RawUI.WindowTitle = if ($isElevated) { + $BaseWindowTitle + " (Admin)" +} else { + $BaseWindowTitle + " (User)" +} + +# Relaunch the script as administrator if necessary +try { + if (!$isElevated) { + Write-Host "WinUtil is not running as administrator. Relaunching..." -ForegroundColor DarkCyan + Start-Process $processCmd -ArgumentList $launchArguments -Verb RunAs + break + } else { + Write-Host "Running the latest stable version of WinUtil as admin." -ForegroundColor DarkCyan + } +} catch { + Write-Host "Error launching WinUtil: $_" -ForegroundColor Red + break +} + +# Start WinUtil transcript logging-mm-ss" $logdir = "$env:localappdata\winutil\logs" [System.IO.Directory]::CreateDirectory("$logdir") | Out-Null -Start-Transcript -Path "$logdir\winutil_$dateTime.log" -Append -NoClobber | Out-Null - -# Set PowerShell window title -$Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Admin)" -clear-host +Start-Transcript -Path "$logdir\winutil_$dateTime.log" -Append -NoClobber | Out-Null \ No newline at end of file diff --git a/windev.ps1 b/windev.ps1 index 9668d2a7..e38eb2a4 100644 --- a/windev.ps1 +++ b/windev.ps1 @@ -1,55 +1,169 @@ <# .SYNOPSIS - This Script is used as a target for the https://christitus.com/windev alias. - It queries the latest winget release (no matter if Pre-Release, Draft or Full Release) and invokes It + This script is used as a target for the https://christitus.com/windev alias. + It queries the latest WinUtil release (no matter if it's a Pre-Release, Draft, or Full Release) and runs it. .DESCRIPTION - This Script provides a simple way to always start the bleeding edge release even if it's not yet a full release. - This function should be run with administrative privileges. - Because this way of recursively invoking scripts via Invoke-Expression it might very well happen that AV Programs flag this because it's a common way of mulitstage exploits to run + This script provides a simple way to start the bleeding edge release even if it's not yet a full release. + This function can be run both with administrator and non-administrator permissions. + If it is not running as administrator, it will attempt to relaunch itself with administrator permissions. + The script no longer uses Invoke-Expression for its execution and now relies on Start-Process. .EXAMPLE - irm https://christitus.com/windev | iex + Run in PowerShell > irm https://christitus.com/windev | iex OR - Run in Admin Powershell > ./windev.ps1 + Run in PowerShell > .\windev.ps1 + OR + Run in PowerShell > iex "& { $(irm https://christitus.com/windev) } " + OR + Run in PowerShell > .\windev.ps1 #> -# Function to fetch the latest release tag from the GitHub API -function Get-LatestRelease { +# Define the arguments for the WinUtil script; enables argument auto-completion. +param ( + [switch]$Debug, + [string]$Config, + [switch]$Run +) + +# Speed up download-related tasks by suppressing the output of Write-Progress. +$ProgressPreference = "SilentlyContinue" + +# Determine whether or not the active process is currently running as administrator. +$ProcessIsElevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + +# Function to get the latest stable or pre-release tag from the repository's releases page. +function Get-WinUtilReleaseTag { + # Retrieve the list of released versions from the repository's releases page. try { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/ChrisTitusTech/winutil/releases' - $latestRelease = $releases | Where-Object {$_.prerelease -eq $true} | Select-Object -First 1 - return $latestRelease.tag_name + $ReleasesList = Invoke-RestMethod 'https://api.github.com/repos/ChrisTitusTech/winutil/releases' } catch { - Write-Host "Error fetching release data: $_" -ForegroundColor Red - return $latestRelease.tag_name + Write-Host "Error downloading WinUtil's releases list: $_" -ForegroundColor Red + break } -} -# Function to redirect to the latest pre-release version -function RedirectToLatestPreRelease { - $latestRelease = Get-LatestRelease - if ($latestRelease) { - $url = "https://github.com/ChrisTitusTech/winutil/releases/download/$latestRelease/winutil.ps1" + # Filter through the released versions and select the first matching stable or pre-release version. + $StableRelease = $ReleasesList | Where-Object { -not $_.prerelease } | Select-Object -First 1 + $PreRelease = $ReleasesList | Where-Object { $_.prerelease } | Select-Object -First 1 + + # Set the release tag based on the available releases; if no release tag is found use the 'latest' tag. + $ReleaseTag = if ($PreRelease) { + $PreRelease.tag_name + } elseif ($StableRelease) { + $StableRelease.tag_name } else { - Write-Host 'Unable to determine latest pre-release version.' -ForegroundColor Red - Write-Host "Using latest Full Release" - $url = "https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1" + "latest" } - $script = Invoke-RestMethod $url - # Elevate Shell if necessary - if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Output "Winutil needs to be run as Administrator. Attempting to relaunch." + # Return the release tag to facilitate the usage of the version number within other parts of the script. + return $ReleaseTag +} - $powershellcmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" } - $processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $powershellcmd } +# Get the latest stable or pre-release tag; falling back to the 'latest' release tag if no releases are found. +$WinUtilReleaseTag = Get-WinUtilReleaseTag - Start-Process $processCmd -ArgumentList "$powershellcmd -ExecutionPolicy Bypass -NoProfile -Command $(Invoke-Expression $script)" -Verb RunAs +# Function to generate the URL used to download the latest release of WinUtil from the releases page. +function Get-WinUtilReleaseURL { + $WinUtilDownloadURL = if ($WinUtilReleaseTag -eq "latest") { + "https://github.com/ChrisTitusTech/winutil/releases/$($WinUtilReleaseTag)/download/winutil.ps1" + } else { + "https://github.com/ChrisTitusTech/winutil/releases/download/$($WinUtilReleaseTag)/winutil.ps1" } - else{ - Invoke-Expression $script + + return $WinUtilDownloadURL +} + +# Get the URL used to download the latest release of WinUtil from the releases page. +$WinUtilReleaseURL = Get-WinUtilReleaseURL + +# Create the file path that the latest WinUtil release will be downloaded to on the local disk. +$WinUtilScriptPath = Join-Path "$env:TEMP" "winutil.ps1" + +# Function to download the latest release of WinUtil from the releases page to the local disk. +function Get-LatestWinUtil { + if (!(Test-Path $WinUtilScriptPath)) { + Write-Host "WinUtil is not found. Downloading WinUtil '$($WinUtilReleaseTag)'..." -ForegroundColor DarkYellow + Invoke-RestMethod $WinUtilReleaseURL -OutFile $WinUtilScriptPath } } -# Call the redirect function +# Function to check for any version changes to WinUtil, re-downloading the script if it has been upgraded/downgraded. +function Get-WinUtilUpdates { + # Make a web request to the latest WinUtil release URL and store the script's raw code for processing. + $RawWinUtilScript = (Invoke-WebRequest $WinUtilReleaseURL -UseBasicParsing).RawContent -RedirectToLatestPreRelease + # Create the regular expression pattern used to extract the script's version number from the script's raw content. + $VersionExtractionRegEx = "(\bVersion\s*:\s)([\d.]+)" + + # Match the entire 'Version:' header and extract the script's version number directly using RegEx capture groups. + $RemoteWinUtilVersion = (([regex]($VersionExtractionRegEx)).Match($RawWinUtilScript).Groups[2].Value) + $LocalWinUtilVersion = (([regex]($VersionExtractionRegEx)).Match((Get-Content $WinUtilScriptPath)).Groups[2].Value) + + # Check if WinUtil needs an update and either download a fresh copy or notify the user its already up-to-date. + if ([version]$RemoteWinUtilVersion -ne [version]$LocalWinUtilVersion) { + Write-Host "WinUtil is out-of-date. Downloading WinUtil '$($RemoteWinUtilVersion)'..." -ForegroundColor DarkYellow + Invoke-RestMethod $WinUtilReleaseURL -OutFile $WinUtilScriptPath + } else { + Write-Host "WinUtil is already up-to-date. Skipped update checking." -ForegroundColor Yellow + } +} + +# Function to start the latest release of WinUtil that was previously downloaded and saved to '$env:TEMP\winutil.ps1'. +function Start-LatestWinUtil { + param ( + [Parameter(Mandatory = $false)] + [array]$WinUtilArgumentsList + ) + + # Setup the commands used to launch WinUtil using Windows Terminal or Windows PowerShell/PowerShell Core. + $PowerShellCommand = if (Get-Command pwsh.exe -ErrorAction SilentlyContinue) { "pwsh.exe" } else { "powershell.exe" } + $ProcessCommand = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $PowerShellCommand } + + # Setup the script's launch arguments based on the presence of Windows Terminal or Windows PowerShell/PowerShell Core: + # 1. Windows Terminal needs the name of the process to start ($PowerShellCommand) in addition to the launch arguments. + # 2. Windows PowerShell and PowerShell Core can receive and use the launch arguments as is without extra modification. + $WinUtilLaunchArguments = "-ExecutionPolicy Bypass -NoProfile -File `"$WinUtilScriptPath`"" + if ($ProcessCommand -ne $PowerShellCommand) { + $WinUtilLaunchArguments = "$PowerShellCommand $WinUtilLaunchArguments" + } + + # If WinUtil's launch arguments are provided, append them to the end of the list of current launch arguments. + if ($WinUtilArgumentsList) { + $WinUtilLaunchArguments += " " + $($WinUtilArgumentsList -join " ") + } + + # If the WinUtil script is not running as administrator, relaunch the script with administrator permissions. + if (!$ProcessIsElevated) { + Write-Host "WinUtil is not running as administrator. Relaunching..." -ForegroundColor DarkCyan + Write-Host "Running the selected WinUtil release: Version '$($WinUtilReleaseTag)'." -ForegroundColor Green + Start-Process $ProcessCommand -ArgumentList $WinUtilLaunchArguments -Verb RunAs + } else { + Write-Host "Running the selected WinUtil release: Version '$($WinUtilReleaseTag)'." -ForegroundColor Green + Start-Process $ProcessCommand -ArgumentList $WinUtilLaunchArguments + } +} + +# If WinUtil has not already been downloaded, attempt to download it and save it to '$env:TEMP\winutil.ps1'. +try { + Get-LatestWinUtil +} catch { + Write-Host "Error downloading WinUtil '$($WinUtilReleaseTag)': $_" -ForegroundColor Red + break +} + +# Attempt to check for WinUtil updates, if a version is released or removed this will re-download WinUtil. +try { + Get-WinUtilUpdates +} catch { + Write-Host "Error updating WinUtil '$($WinUtilReleaseTag)': $_" -ForegroundColor Red + break +} + +# Attempt to start the latest release of WinUtil saved to the local disk; also supports WinUtil's arguments. +try { + if ($args) { + Start-LatestWinUtil $args + } else { + Start-LatestWinUtil + } +} catch { + Write-Host "Error launching WinUtil '$($WinUtilReleaseTag)': $_" -ForegroundColor Red +} \ No newline at end of file