microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/lib/Get-VerifiedDownload.ps1

383lines · modecode

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