-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.ps1
More file actions
243 lines (213 loc) · 11.7 KB
/
Copy pathinstall.ps1
File metadata and controls
243 lines (213 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
#Requires -Version 5.1
<#
.SYNOPSIS
WinClean installer: install or update locally and create an elevated desktop shortcut
.DESCRIPTION
Install WinClean with a single command from an elevated terminal:
irm https://raw.githubusercontent.com/bivlked/WinClean/main/install.ps1 | iex
Downloads the latest GitHub Release into %ProgramFiles%\WinClean, verifies its
SHA256 against the published hash, and creates a desktop shortcut that runs the
script elevated. Re-running updates an existing installation in place.
Fails closed. A release that does not publish BOTH WinClean.ps1 and
WinClean.ps1.sha256 is refused, and there is no fallback to a branch or a tag.
Requirements: PowerShell 7.1+ installed at %ProgramFiles%\PowerShell\7 and an
elevated terminal (installation writes to Program Files).
.NOTES
Project: https://github.com/bivlked/WinClean
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$repo = 'bivlked/WinClean'
# Windows PowerShell 5.1 defaults to TLS 1.0/1.1 on older builds, which api.github.com
# refuses. The installer must work from whatever shell the user has before PS7 exists.
if ($PSVersionTable.PSEdition -eq 'Desktop') {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$PSDefaultParameterValues['Invoke-WebRequest:UseBasicParsing'] = $true
$PSDefaultParameterValues['Invoke-RestMethod:UseBasicParsing'] = $true
}
function Stop-Install {
<# Reports a failure in a way automation can detect, without killing an `iex` host session #>
param([string]$Message, [string]$Hint, [int]$Code = 1)
Write-Host $Message -ForegroundColor Red
if ($Hint) { Write-Host $Hint -ForegroundColor Yellow }
Write-Error $Message -ErrorAction Continue
$global:LASTEXITCODE = $Code
}
function Assert-GitHubUri {
param([string]$Uri)
# Exact-host allowlist (v2.18). A release browser_download_url is always github.com;
# the old suffix match accepted any *.github.com / *.githubusercontent.com subdomain
# this never needs. Redirects to the asset CDN are followed internally and not
# re-validated here, so CDN hosts are deliberately excluded.
$allowedHosts = @('github.com')
$parsed = [uri]$Uri
if ($parsed.Scheme -ne 'https' -or $parsed.Host -notin $allowedHosts) {
throw "Refusing to download from an unexpected host: $($parsed.Host)"
}
return $Uri
}
# 1. Administrator (required: install target is %ProgramFiles%)
$principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Stop-Install "The installer must run as Administrator (it writes to Program Files)." `
"Open an elevated terminal (Win+X -> Terminal (Admin)) and re-run the command."
return
}
# 2. PowerShell 7 at the canonical location (the shortcut target).
# Deliberately NOT resolved from PATH, and NOT from $env:ProgramFiles: user
# environment variables override machine ones and are writable by a non-admin
# process, which would let it point an elevated shortcut at its own binary.
$programFiles = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles)
$pwshPath = Join-Path $programFiles 'PowerShell\7\pwsh.exe'
if (-not (Test-Path $pwshPath)) {
Stop-Install "PowerShell 7 not found at $pwshPath - WinClean requires it." `
"Install it with: winget install --id Microsoft.PowerShell"
return
}
# The version has to be PROVEN, not merely "not disproven" (raised in external review).
# The previous form was `if ($pwshVersion -and $pwshVersion -lt '7.1')`, so a version that
# could not be read left $pwshVersion $null, skipped the comparison and continued - the
# installer then pinned an elevated desktop shortcut to a binary whose suitability nobody
# had established. That is the same fail-open shape as the SHA256 verification that hid
# inside `if ($hashAsset)` until v2.17: a check that cannot run becomes a check that
# passes. Absence of evidence is not evidence of compatibility.
#
# Read the NUMERIC version fields rather than parsing the display string. pwsh.exe reports
# ProductVersion as "7.6.4 SHA: 929d27f4...+929d27f4..." - the commit hash is appended after
# a SPACE, so stripping a "-suffix" (which only covers preview builds like "7.7.0-preview.2")
# left the hash in place and the [version] cast threw on every released PowerShell 7. Making
# the check fail-closed above is what surfaced it: the parse had never worked, and the old
# form skipped the comparison instead of reporting it, so the failure was invisible for three
# releases. ProductMajorPart/MinorPart/BuildPart are integers taken straight from the version
# resource - there is no string to misparse.
$pwshVersion = try {
$vi = (Get-Item $pwshPath).VersionInfo
# A genuine PowerShell 7 binary never reports major 0; that value means the version
# resource is missing or unreadable, which must stay fail-closed.
if ($vi.ProductMajorPart -gt 0) {
[version]::new($vi.ProductMajorPart, $vi.ProductMinorPart, $vi.ProductBuildPart)
} else { $null }
} catch { $null }
if (-not $pwshVersion) {
# Deliberately a different message from "your version is too old": the user needs to
# know the file is there but unreadable, which points at a damaged or substituted
# install rather than at an outdated one.
Stop-Install "PowerShell 7 was found at $pwshPath, but its version could not be read - WinClean requires 7.1+ and will not assume it." `
"Repair or reinstall PowerShell 7: winget install --id Microsoft.PowerShell --force"
return
}
if ($pwshVersion -lt [version]'7.1') {
Stop-Install "PowerShell $pwshVersion found at $pwshPath, but WinClean requires 7.1+." `
"Update it with: winget upgrade --id Microsoft.PowerShell"
return
}
# 3. Resolve the latest release
try {
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" -TimeoutSec 15
} catch {
$status = $_.Exception.Response.StatusCode.value__
$hint = if ($status -in 403, 429) {
"GitHub API rate limit reached for your address. Wait an hour or download manually: https://github.com/$repo/releases"
} else {
"Check your connection and try again, or download manually: https://github.com/$repo/releases"
}
Stop-Install "Could not query the latest WinClean release: $_" $hint
return
}
$scriptAsset = $release.assets | Where-Object { $_.name -eq 'WinClean.ps1' } | Select-Object -First 1
$hashAsset = $release.assets | Where-Object { $_.name -eq 'WinClean.ps1.sha256' } | Select-Object -First 1
# Both assets are mandatory - see get.ps1 for the reasoning
if (-not $scriptAsset -or -not $hashAsset) {
Stop-Install "Release $($release.tag_name) does not publish both WinClean.ps1 and WinClean.ps1.sha256." `
"Refusing to install unverified code. Download and check manually: https://github.com/$repo/releases"
return
}
# 4. Download and verify, then move into place
$installDir = Join-Path $programFiles 'WinClean'
$scriptPath = Join-Path $installDir 'WinClean.ps1'
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
$previousVersion = $null
if (Test-Path $scriptPath) {
$versionLine = Select-String -Path $scriptPath -Pattern '^\.VERSION\s+([\d.]+)' | Select-Object -First 1
if ($versionLine) { $previousVersion = $versionLine.Matches[0].Groups[1].Value }
}
$tempFile = Join-Path $installDir 'WinClean.ps1.download'
$hashFile = "$tempFile.sha256"
try {
Write-Host "Downloading WinClean $($release.tag_name)..." -ForegroundColor Cyan
Invoke-WebRequest -Uri (Assert-GitHubUri $scriptAsset.browser_download_url) `
-OutFile $tempFile -TimeoutSec 60 -MaximumRedirection 3
Invoke-WebRequest -Uri (Assert-GitHubUri $hashAsset.browser_download_url) `
-OutFile $hashFile -TimeoutSec 30 -MaximumRedirection 3
$expected = ((Get-Content -LiteralPath $hashFile -Raw) -split '\s+')[0].Trim()
if ($expected -notmatch '^[0-9a-fA-F]{64}$') {
Stop-Install "The published hash is not a valid SHA256 value. Aborting."
return
}
$actual = (Get-FileHash -LiteralPath $tempFile -Algorithm SHA256).Hash
# Literal comparison: -like would treat the published hash as a wildcard pattern
if (-not [string]::Equals($actual, $expected, [System.StringComparison]::OrdinalIgnoreCase)) {
Stop-Install "SHA256 mismatch - the downloaded file does not match the published hash. Aborting."
return
}
Write-Host "SHA256 verified." -ForegroundColor DarkGray
# The hash proves the two assets agree with each other, not that the asset is
# WinClean. A packaging mistake in the release would otherwise replace a working
# installation with arbitrary content.
$head = Get-Content -LiteralPath $tempFile -TotalCount 5 -ErrorAction Stop
if (-not ($head -join "`n").Contains('PSScriptInfo')) {
Stop-Install "The downloaded asset does not look like WinClean.ps1 - keeping the existing installation."
return
}
try {
Move-Item -LiteralPath $tempFile -Destination $scriptPath -Force
} catch {
Stop-Install "Could not replace $scriptPath : $_" `
"Close any running WinClean window and re-run the installer."
return
}
} finally {
Remove-Item $tempFile, $hashFile -Force -ErrorAction SilentlyContinue
}
$newVersion = '?'
$versionLine = Select-String -Path $scriptPath -Pattern '^\.VERSION\s+([\d.]+)' | Select-Object -First 1
if ($versionLine) { $newVersion = $versionLine.Matches[0].Groups[1].Value }
if ($previousVersion) {
Write-Host "Updated: $previousVersion -> $newVersion" -ForegroundColor Green
} else {
Write-Host "Installed version $newVersion to $installDir" -ForegroundColor Green
}
# 5. Desktop shortcut that runs elevated.
# Missing Desktop is not fatal: under SYSTEM or a redirected profile there may be
# none, and the installation itself is already complete and usable.
try {
$desktop = [Environment]::GetFolderPath('Desktop')
if ([string]::IsNullOrWhiteSpace($desktop)) { throw "no Desktop folder for the current user" }
# An argument string is built by interpolation, so a quote in the path would be
# an injection into the command line of an elevated shortcut
if ($scriptPath -match '["`$]') { throw "unsafe characters in the install path: $scriptPath" }
$lnkPath = Join-Path $desktop 'WinClean.lnk'
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($lnkPath)
$shortcut.TargetPath = $pwshPath
$shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$shortcut.WorkingDirectory = $installDir
$shortcut.IconLocation = "$pwshPath,0"
$shortcut.Description = "WinClean - Windows 11 maintenance"
$shortcut.Save()
# Set the "Run as administrator" flag (bit 0x20 of byte 0x15 in the .lnk header)
$lnkBytes = [System.IO.File]::ReadAllBytes($lnkPath)
$lnkBytes[0x15] = $lnkBytes[0x15] -bor 0x20
[System.IO.File]::WriteAllBytes($lnkPath, $lnkBytes)
Write-Host "Desktop shortcut created (runs elevated): $lnkPath" -ForegroundColor Green
} catch {
Write-Host "Shortcut not created ($_)." -ForegroundColor Yellow
Write-Host "WinClean is installed and can be run directly:" -ForegroundColor Yellow
Write-Host " & '$pwshPath' -NoProfile -File '$scriptPath'" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Run it from the shortcut, or:" -ForegroundColor Cyan
Write-Host " & '$scriptPath' -ReportOnly # preview without changes" -ForegroundColor Gray
Write-Host " & '$scriptPath' # full maintenance" -ForegroundColor Gray
Write-Host ""