microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v3.3.41

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/lib/Get-VerifiedDownload.ps1

394lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3#Requires -Version 7.0
4
5<#
6.SYNOPSIS
7 Downloads and verifies artifacts using SHA256 checksums.
8
9.DESCRIPTION
10 Securely downloads files from URLs and verifies their integrity using
11 SHA256 checksums before saving or extracting. Contains pure functions
12 for testability and an I/O wrapper for orchestration.
13
14.PARAMETER Url
15 URL to download from.
16
17.PARAMETER ExpectedSHA256
18 Expected SHA256 checksum of the file.
19
20.PARAMETER OutputPath
21 Path where the downloaded file will be saved.
22
23.PARAMETER Extract
24 Extract the archive after verification.
25
26.PARAMETER ExtractPath
27 Destination directory for extraction.
28
29.EXAMPLE
30 .\Get-VerifiedDownload.ps1 -Url "https://example.com/tool.tar.gz" -ExpectedSHA256 "abc123..." -OutputPath "./tool.tar.gz"
31
32.EXAMPLE
33 .\Get-VerifiedDownload.ps1 -Url "https://example.com/tool.tar.gz" -ExpectedSHA256 "abc123..." -OutputPath "./tool.tar.gz" -Extract -ExtractPath "./tools"
34
35.EXAMPLE
36 . .\Get-VerifiedDownload.ps1
37 Invoke-VerifiedDownload -Url "https://example.com/file.zip" -DestinationDirectory "C:\downloads" -ExpectedHash "abc123..."
38#>
39
40#region Script Parameters
41
42[CmdletBinding()]
43param(
44 [Parameter(Mandatory = $false)]
45 [string]$Url,
46
47 [Parameter(Mandatory = $false)]
48 [string]$ExpectedSHA256,
49
50 [Parameter(Mandatory = $false)]
51 [string]$OutputPath,
52
53 [Parameter(Mandatory = $false)]
54 [switch]$Extract,
55
56 [Parameter(Mandatory = $false)]
57 [string]$ExtractPath
58)
59
60#endregion
61
62$ErrorActionPreference = 'Stop'
63
64Import-Module (Join-Path $PSScriptRoot "Modules/CIHelpers.psm1") -Force
65
66#region Pure Functions
67
68function Get-FileHashValue {
69 <#
70 .SYNOPSIS
71 Computes the hash of a file using the specified algorithm.
72 .OUTPUTS
73 System.String
74 #>
75 [CmdletBinding()]
76 [OutputType([string])]
77 param(
78 [Parameter(Mandatory)]
79 [string]$Path,
80
81 [Parameter(Mandatory)]
82 [ValidateSet('SHA256', 'SHA384', 'SHA512')]
83 [string]$Algorithm
84 )
85
86 $hashResult = Get-FileHash -Path $Path -Algorithm $Algorithm
87 return $hashResult.Hash
88}
89
90function Test-HashMatch {
91 <#
92 .SYNOPSIS
93 Compares two hash strings for equality (case-insensitive).
94 .OUTPUTS
95 System.Boolean
96 #>
97 [CmdletBinding()]
98 [OutputType([bool])]
99 param(
100 [Parameter(Mandatory)]
101 [string]$ComputedHash,
102
103 [Parameter(Mandatory)]
104 [string]$ExpectedHash
105 )
106
107 return $ComputedHash.ToUpperInvariant() -eq $ExpectedHash.ToUpperInvariant()
108}
109
110function Get-DownloadTargetPath {
111 <#
112 .SYNOPSIS
113 Resolves the target file path for a download operation.
114 .OUTPUTS
115 System.String
116 #>
117 [CmdletBinding()]
118 [OutputType([string])]
119 param(
120 [Parameter(Mandatory)]
121 [string]$Url,
122
123 [Parameter(Mandatory)]
124 [string]$DestinationDirectory,
125
126 [Parameter()]
127 [string]$FileName
128 )
129
130 if ([string]::IsNullOrWhiteSpace($FileName)) {
131 $uri = [System.Uri]::new($Url)
132 $FileName = [System.IO.Path]::GetFileName($uri.LocalPath)
133 }
134
135 return [System.IO.Path]::Combine($DestinationDirectory, $FileName)
136}
137
138function Test-ExistingFileValid {
139 <#
140 .SYNOPSIS
141 Checks if an existing file matches the expected hash.
142 .OUTPUTS
143 System.Boolean
144 #>
145 [CmdletBinding()]
146 [OutputType([bool])]
147 param(
148 [Parameter(Mandatory)]
149 [string]$Path,
150
151 [Parameter(Mandatory)]
152 [string]$ExpectedHash,
153
154 [Parameter(Mandatory)]
155 [ValidateSet('SHA256', 'SHA384', 'SHA512')]
156 [string]$Algorithm
157 )
158
159 if (-not (Test-Path -Path $Path -PathType Leaf)) {
160 return $false
161 }
162
163 $computedHash = Get-FileHashValue -Path $Path -Algorithm $Algorithm
164 return Test-HashMatch -ComputedHash $computedHash -ExpectedHash $ExpectedHash
165}
166
167function New-DownloadResult {
168 <#
169 .SYNOPSIS
170 Creates a standardized download result object.
171 .OUTPUTS
172 System.Collections.Hashtable
173 #>
174 [CmdletBinding()]
175 [OutputType([hashtable])]
176 param(
177 [Parameter(Mandatory)]
178 [string]$Path,
179
180 [Parameter(Mandatory)]
181 [bool]$WasDownloaded,
182
183 [Parameter(Mandatory)]
184 [bool]$HashVerified
185 )
186
187 return @{
188 Path = $Path
189 WasDownloaded = $WasDownloaded
190 HashVerified = $HashVerified
191 }
192}
193
194function Get-ArchiveType {
195 <#
196 .SYNOPSIS
197 Determines the archive type from a URL or file path.
198 .OUTPUTS
199 System.String
200 #>
201 [CmdletBinding()]
202 [OutputType([string])]
203 param(
204 [Parameter(Mandatory)]
205 [string]$Path
206 )
207
208 switch -Regex ($Path) {
209 '\.zip$' { return 'zip' }
210 '\.(tar\.gz|tgz)$' { return 'tar.gz' }
211 '\.tar$' { return 'tar' }
212 default { return 'unknown' }
213 }
214}
215
216function Test-TarAvailable {
217 <#
218 .SYNOPSIS
219 Tests if the tar command is available.
220 .OUTPUTS
221 System.Boolean
222 #>
223 [CmdletBinding()]
224 [OutputType([bool])]
225 param()
226
227 $tarCmd = Get-Command -Name 'tar' -ErrorAction SilentlyContinue
228 return $null -ne $tarCmd
229}
230
231#endregion
232
233#region I/O Wrapper Function
234
235function Invoke-VerifiedDownload {
236 <#
237 .SYNOPSIS
238 Downloads and verifies a file with hash validation.
239 .DESCRIPTION
240 I/O wrapper that orchestrates download operations using pure functions
241 for logic and handles all file system and network operations.
242 .OUTPUTS
243 System.Collections.Hashtable
244 #>
245 [CmdletBinding()]
246 [OutputType([hashtable])]
247 param(
248 [Parameter(Mandatory)]
249 [string]$Url,
250
251 [Parameter(Mandatory)]
252 [string]$DestinationDirectory,
253
254 [Parameter(Mandatory)]
255 [string]$ExpectedHash,
256
257 [Parameter()]
258 [ValidateSet('SHA256', 'SHA384', 'SHA512')]
259 [string]$Algorithm = 'SHA256',
260
261 [Parameter()]
262 [string]$FileName,
263
264 [Parameter()]
265 [switch]$Extract,
266
267 [Parameter()]
268 [string]$ExtractPath
269 )
270
271 $targetPath = Get-DownloadTargetPath -Url $Url -DestinationDirectory $DestinationDirectory -FileName $FileName
272
273 # Check if valid file already exists
274 if (Test-Path $targetPath) {
275 if (Test-ExistingFileValid -Path $targetPath -ExpectedHash $ExpectedHash -Algorithm $Algorithm) {
276 Write-Verbose "File already exists and hash matches: $targetPath"
277 return New-DownloadResult -Path $targetPath -WasDownloaded $false -HashVerified $true
278 }
279 }
280
281 # Ensure destination directory exists
282 if (-not (Test-Path $DestinationDirectory)) {
283 New-Item -ItemType Directory -Path $DestinationDirectory -Force | Out-Null
284 }
285
286 # Download to temp file first
287 $tempFile = [System.IO.Path]::GetTempFileName()
288 try {
289 Write-Host "Downloading: $Url"
290 Invoke-WebRequest -Uri $Url -OutFile $tempFile -UseBasicParsing
291
292 $computedHash = Get-FileHashValue -Path $tempFile -Algorithm $Algorithm
293 $verified = Test-HashMatch -ComputedHash $computedHash -ExpectedHash $ExpectedHash
294
295 if (-not $verified) {
296 throw "Checksum verification failed!`nExpected: $ExpectedHash`nActual: $computedHash"
297 }
298
299 # Handle extraction or move
300 if ($Extract) {
301 $extractDir = if ($ExtractPath) { $ExtractPath } else { $DestinationDirectory }
302 if (-not (Test-Path $extractDir)) {
303 New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
304 }
305
306 $archiveType = Get-ArchiveType -Path $Url
307 switch ($archiveType) {
308 'zip' {
309 Write-Verbose "Extracting ZIP archive to $extractDir"
310 Expand-Archive -Path $tempFile -DestinationPath $extractDir -Force
311 }
312 'tar.gz' {
313 if (-not (Test-TarAvailable)) {
314 throw "tar command not available for .tar.gz extraction"
315 }
316 Write-Verbose "Extracting tar.gz archive to $extractDir"
317 tar -xzf $tempFile -C $extractDir
318 if ($LASTEXITCODE -ne 0) {
319 throw "tar extraction failed with exit code $LASTEXITCODE"
320 }
321 }
322 'tar' {
323 if (-not (Test-TarAvailable)) {
324 throw "tar command not available for .tar extraction"
325 }
326 Write-Verbose "Extracting tar archive to $extractDir"
327 tar -xf $tempFile -C $extractDir
328 if ($LASTEXITCODE -ne 0) {
329 throw "tar extraction failed with exit code $LASTEXITCODE"
330 }
331 }
332 default {
333 throw "Unsupported archive format for '$Url'. Supported: .zip, .tar.gz, .tgz, .tar"
334 }
335 }
336 }
337 else {
338 Move-Item -Path $tempFile -Destination $targetPath -Force
339 }
340
341 Write-Host "Download verified and complete" -ForegroundColor Green
342 return New-DownloadResult -Path $targetPath -WasDownloaded $true -HashVerified $true
343 }
344 finally {
345 if (Test-Path $tempFile) {
346 Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
347 }
348 }
349}
350
351#endregion
352
353#region Main Execution
354if ($MyInvocation.InvocationName -ne '.') {
355 try {
356 # Require parameters for direct invocation
357 if (-not $Url -or -not $ExpectedSHA256 -or -not $OutputPath) {
358 Write-Error "When invoking directly, -Url, -ExpectedSHA256, and -OutputPath are required."
359 exit 1
360 }
361
362 # Resolve destination directory and file name from OutputPath
363 $destinationDir = Split-Path -Parent $OutputPath
364 if (-not $destinationDir) {
365 $destinationDir = $PWD.Path
366 }
367 $fileName = Split-Path -Leaf $OutputPath
368
369 # Determine extract path
370 $extractDir = $null
371 if ($Extract) {
372 $extractDir = if ($ExtractPath) { $ExtractPath } else { $destinationDir }
373 }
374
375 # Call the I/O wrapper function with script parameters
376 $result = Invoke-VerifiedDownload `
377 -Url $Url `
378 -DestinationDirectory $destinationDir `
379 -ExpectedHash $ExpectedSHA256 `
380 -FileName $fileName `
381 -Extract:$Extract `
382 -ExtractPath $extractDir
383
384 # Output the result for callers
385 $result
386 exit 0
387 }
388 catch {
389 Write-Error -ErrorAction Continue "Get-VerifiedDownload failed: $($_.Exception.Message)"
390 Write-CIAnnotation -Message $_.Exception.Message -Level Error
391 exit 1
392 }
393}
394#endregion Main Execution
395