Support/Package/Schema/Eigenverft.Manifested.Package.Package.Source.ps1
|
<#
Eigenverft.Manifested.Package.Package.Source #> function Get-PackageSourceDefinition { <# .SYNOPSIS Returns a resolved Package source definition by sourceRef. .DESCRIPTION Looks up an acquisition source from the effective acquisition environment or from definition-local artifact sources and returns the normalized source definition with scope and id metadata. .PARAMETER PackageConfig The resolved Package config object. .PARAMETER SourceRef The acquisition-candidate sourceRef object. .EXAMPLE Get-PackageSourceDefinition -PackageConfig $config -SourceRef $candidate.sourceRef #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageConfig, [Parameter(Mandatory = $true)] [psobject]$SourceRef ) $scope = [string]$SourceRef.scope $id = [string]$SourceRef.id $sourceObject = $null switch -Exact ($scope) { 'environment' { foreach ($property in @($PackageConfig.EnvironmentSources.PSObject.Properties)) { if ([string]::Equals([string]$property.Name, $id, [System.StringComparison]::OrdinalIgnoreCase)) { $sourceObject = $property.Value $id = $property.Name break } } if (-not $sourceObject) { throw "Package environment source '$($SourceRef.id)' was not found in the effective acquisition environment." } } 'definition' { foreach ($property in @($PackageConfig.DefinitionSources.PSObject.Properties)) { if ([string]::Equals([string]$property.Name, $id, [System.StringComparison]::OrdinalIgnoreCase)) { $sourceObject = $property.Value $id = $property.Name break } } if (-not $sourceObject) { throw "Package definition source '$($SourceRef.id)' was not found in definition '$($PackageConfig.DefinitionId)'." } } default { throw "Unsupported Package sourceRef.scope '$scope'." } } return [pscustomobject]@{ Scope = $scope Id = $id Kind = if ($sourceObject.PSObject.Properties['kind']) { [string]$sourceObject.kind } else { $null } BaseUri = if ($sourceObject.PSObject.Properties['baseUri']) { [string]$sourceObject.baseUri } else { $null } BasePath = if ($sourceObject.PSObject.Properties['basePath']) { [string]$sourceObject.basePath } else { $null } GitHubOwner = if ($sourceObject.PSObject.Properties['githubOwner']) { [string]$sourceObject.githubOwner } else { $null } GitHubRepository = if ($sourceObject.PSObject.Properties['githubRepository']) { [string]$sourceObject.githubRepository } else { $null } } } function Resolve-PackageSource { <# .SYNOPSIS Resolves a concrete source location from a source definition and acquisition candidate. .DESCRIPTION Combines a resolved source definition with one release acquisition candidate and returns the concrete URI or filesystem path that should be used for the package-file save. .PARAMETER SourceDefinition The resolved source definition for the acquisition candidate. .PARAMETER AcquisitionCandidate The release acquisition candidate. .PARAMETER Package The selected effective release object. Required for source kinds that resolve through release metadata, such as GitHub release lookup by tag. .EXAMPLE Resolve-PackageSource -SourceDefinition $source -AcquisitionCandidate $candidate #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$SourceDefinition, [Parameter(Mandatory = $true)] [psobject]$AcquisitionCandidate, [AllowNull()] [psobject]$Package, [AllowNull()] [psobject]$ArtifactFile ) switch -Exact ([string]$SourceDefinition.Kind) { 'download' { if ($AcquisitionCandidate.PSObject.Properties['url'] -and -not [string]::IsNullOrWhiteSpace([string]$AcquisitionCandidate.url)) { return [pscustomobject]@{ Kind = 'download' ResolvedSource = [string]$AcquisitionCandidate.url } } if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.BaseUri)) { throw "Package download source '$($SourceDefinition.Id)' does not define baseUri. Use sourcePath with sourceId, or use acquisitionCandidate.url/urlTemplate for direct downloads." } if (-not $AcquisitionCandidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$AcquisitionCandidate.sourcePath)) { throw "Package acquisition candidate for '$($SourceDefinition.Id)' does not define sourcePath. Use sourcePath, artifact sourcePath, url, or urlTemplate." } $baseUriText = ([string]$SourceDefinition.BaseUri).TrimEnd('/') + '/' $resolvedUri = [System.Uri]::new([System.Uri]$baseUriText, [string]$AcquisitionCandidate.sourcePath) return [pscustomobject]@{ Kind = 'download' ResolvedSource = $resolvedUri.AbsoluteUri } } 'githubRelease' { if (-not $Package) { throw "Package GitHub release source '$($SourceDefinition.Id)' requires the selected package release context." } if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.GitHubOwner)) { throw "Package GitHub release source '$($SourceDefinition.Id)' does not define githubOwner." } if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.GitHubRepository)) { throw "Package GitHub release source '$($SourceDefinition.Id)' does not define githubRepository." } if (-not $Package.PSObject.Properties['releaseTag'] -or [string]::IsNullOrWhiteSpace([string]$Package.releaseTag)) { throw "Package release '$($Package.id)' requires releaseTag when acquisition uses GitHub release source '$($SourceDefinition.Id)'." } if (-not $ArtifactFile -or [string]::IsNullOrWhiteSpace([string]$ArtifactFile.RelativePath)) { throw "Package release '$($Package.id)' requires artifact file context when acquisition uses GitHub release source '$($SourceDefinition.Id)'." } $release = Get-GitHubRelease -RepositoryOwner $SourceDefinition.GitHubOwner -RepositoryName $SourceDefinition.GitHubRepository -ReleaseTag ([string]$Package.releaseTag) $assetName = Split-Path -Leaf ([string]$ArtifactFile.RelativePath) $matchedAsset = @( $release.Assets | Where-Object { [string]::Equals([string]$_.Name, $assetName, [System.StringComparison]::OrdinalIgnoreCase) } ) | Select-Object -First 1 if (-not $matchedAsset) { throw "GitHub release '$($Package.releaseTag)' for '$($SourceDefinition.GitHubOwner)/$($SourceDefinition.GitHubRepository)' does not contain asset '$assetName'." } if ([string]::IsNullOrWhiteSpace([string]$matchedAsset.DownloadUrl)) { throw "GitHub release '$($Package.releaseTag)' asset '$assetName' for '$($SourceDefinition.GitHubOwner)/$($SourceDefinition.GitHubRepository)' does not expose a download URL." } return [pscustomobject]@{ Kind = 'download' ResolvedSource = [string]$matchedAsset.DownloadUrl } } 'filesystem' { if (-not $AcquisitionCandidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$AcquisitionCandidate.sourcePath)) { throw "Package acquisition candidate for '$($SourceDefinition.Id)' does not define sourcePath." } $sourcePath = ([string]$AcquisitionCandidate.sourcePath).Trim() -replace '/', '\' if ([System.IO.Path]::IsPathRooted($sourcePath)) { $resolvedPath = Resolve-PackagePathValue -PathValue $sourcePath } else { if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.BasePath)) { throw "Package filesystem source '$($SourceDefinition.Id)' does not define basePath." } $resolvedPath = [System.IO.Path]::GetFullPath((Join-Path $SourceDefinition.BasePath $sourcePath)) } return [pscustomobject]@{ Kind = 'filesystem' ResolvedSource = $resolvedPath } } default { throw "Unsupported Package source kind '$($SourceDefinition.Kind)'." } } } function Test-PackageSavedFile { <# .SYNOPSIS Evaluates a package file against a save-time verification policy. .DESCRIPTION Applies the acquisition candidate verification policy to a local file and returns the verification status, whether the file is accepted, and the expected and actual hash values when hashing is performed. .PARAMETER Path The local file path to verify. .PARAMETER Verification The verification policy object from the acquisition candidate. .EXAMPLE Test-PackageSavedFile -Path .\package.zip -Verification $verification #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [AllowNull()] [psobject]$Verification ) if ($Verification -is [System.Collections.IDictionary]) { $Verification = [pscustomobject]$Verification } $authenticode = if ($Verification -and $Verification.PSObject.Properties['authenticode'] -and $null -ne $Verification.authenticode) { if ($Verification.authenticode -is [System.Collections.IDictionary]) { [pscustomobject]$Verification.authenticode } else { $Verification.authenticode } } else { $null } $mode = if ($Verification -and $Verification.PSObject.Properties['mode'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.mode)) { ([string]$Verification.mode).ToLowerInvariant() } else { 'none' } if (-not (Test-Path -LiteralPath $Path)) { return [pscustomobject]@{ Status = 'FileMissing' Accepted = $false Verified = $false Mode = $mode Algorithm = $null ExpectedHash = $null ActualHash = $null } } if ($mode -eq 'none' -and -not $authenticode) { return [pscustomobject]@{ Status = 'VerificationSkipped' Accepted = $true Verified = $false Mode = $mode Algorithm = $null ExpectedHash = $null ActualHash = $null SignatureStatus = $null SignerSubject = $null } } $algorithm = if ($Verification -and $Verification.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.algorithm)) { ([string]$Verification.algorithm).ToLowerInvariant() } else { 'sha256' } $hashAlgorithmName = switch -Exact ($algorithm) { 'sha256' { 'SHA256' } 'sha512' { 'SHA512' } default { $null } } if ([string]::IsNullOrWhiteSpace($hashAlgorithmName)) { return [pscustomobject]@{ Status = 'VerificationAlgorithmUnsupported' Accepted = $false Verified = $false Mode = $mode Algorithm = $algorithm ExpectedHash = $null ActualHash = $null SignatureStatus = $null SignerSubject = $null } } $hashProperty = $algorithm $expectedHash = if ($Verification -and $Verification.PSObject.Properties[$hashProperty] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.$hashProperty)) { ([string]$Verification.$hashProperty).Trim().ToLowerInvariant() } elseif ($Verification -and $Verification.PSObject.Properties['value'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.value)) { ([string]$Verification.value).Trim().ToLowerInvariant() } else { $null } if ([string]::IsNullOrWhiteSpace($expectedHash) -and -not $authenticode) { return [pscustomobject]@{ Status = if ($mode -eq 'required') { 'VerificationHashMissing' } else { 'VerificationHashMissingOptional' } Accepted = ($mode -ne 'required') Verified = $false Mode = $mode Algorithm = $algorithm ExpectedHash = $null ActualHash = $null SignatureStatus = $null SignerSubject = $null } } $actualHash = $null $hashAccepted = $true if (-not [string]::IsNullOrWhiteSpace($expectedHash)) { $actualHash = (Get-FileHash -LiteralPath $Path -Algorithm $hashAlgorithmName).Hash.ToLowerInvariant() $hashAccepted = ($actualHash -eq $expectedHash) } $signatureStatus = $null $signerSubject = $null $authenticodeAccepted = $true if ($authenticode) { $authenticodeAccepted = $false try { $signature = Get-AuthenticodeSignature -FilePath $Path $signatureStatus = $signature.Status.ToString() $signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null } $requiresValid = $true if ($authenticode.PSObject.Properties['requireValid']) { $requiresValid = [bool]$authenticode.requireValid } $authenticodeAccepted = (-not $requiresValid) -or ($signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid) if ($authenticodeAccepted -and $authenticode.PSObject.Properties['subjectContains'] -and -not [string]::IsNullOrWhiteSpace([string]$authenticode.subjectContains)) { $authenticodeAccepted = ($null -ne $signerSubject -and $signerSubject -match [regex]::Escape([string]$authenticode.subjectContains)) } } catch { $signatureStatus = 'Failed' $authenticodeAccepted = $false } } $accepted = $hashAccepted -and $authenticodeAccepted $status = if (-not $hashAccepted) { 'VerificationFailed' } elseif ($authenticode -and -not $authenticodeAccepted) { 'AuthenticodeFailed' } elseif ($authenticode -and [string]::IsNullOrWhiteSpace($expectedHash)) { 'AuthenticodePassed' } else { 'VerificationPassed' } return [pscustomobject]@{ Status = $status Accepted = $accepted Verified = $true Mode = $mode Algorithm = $algorithm ExpectedHash = $expectedHash ActualHash = $actualHash SignatureStatus = $signatureStatus SignerSubject = $signerSubject } } function Save-PackageDownloadFile { <# .SYNOPSIS Downloads a package file to a local path. .DESCRIPTION Uses the module's download helper to fetch a package file from an HTTP or HTTPS source into a staging path for later verification and promotion. .PARAMETER Uri The package download URI. .PARAMETER TargetPath The local staging path that should receive the file. .EXAMPLE Save-PackageDownloadFile -Uri https://example.org/package.zip -TargetPath C:\Temp\package.zip #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Uri, [Parameter(Mandatory = $true)] [string]$TargetPath ) Invoke-WebRequestEx -Uri $Uri -OutFile $TargetPath -UseBasicParsing return (Resolve-Path -LiteralPath $TargetPath -ErrorAction Stop).Path } function Save-PackageFilesystemFile { <# .SYNOPSIS Copies a package file from a filesystem source. .DESCRIPTION Copies a package file from a local or network filesystem path into a staging path for later verification and promotion. .PARAMETER SourcePath The local or network path that contains the package file. .PARAMETER TargetPath The local staging path that should receive the copy. .EXAMPLE Save-PackageFilesystemFile -SourcePath \\server\share\package.zip -TargetPath C:\Temp\package.zip #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourcePath, [Parameter(Mandatory = $true)] [string]$TargetPath ) if (-not (Test-Path -LiteralPath $SourcePath)) { throw "Package filesystem source '$SourcePath' does not exist." } return (Copy-FileToPath -SourcePath $SourcePath -TargetPath $TargetPath -Overwrite) } function Test-PackageArtifactFileAcquisitionRequired { <# .SYNOPSIS Determines whether the selected release has required static artifact files. .DESCRIPTION Interprets the current install kind so acquisition is skipped for install flows that do not consume a saved package file. .PARAMETER Package The selected release object. .EXAMPLE Test-PackageArtifactFileAcquisitionRequired -Package $package #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [psobject]$Package ) return [bool]($Package.PSObject.Properties['artifactFiles'] -and @($Package.artifactFiles).Count -gt 0) } function Get-PackagePreferredVerification { [CmdletBinding()] param( [AllowNull()] [object[]]$AcquisitionCandidates ) foreach ($candidate in @($AcquisitionCandidates)) { if ($candidate.PSObject.Properties['verification'] -and $null -ne $candidate.verification) { return $candidate.verification } } return [pscustomobject]@{ mode = 'none' } } function Resolve-PackageAcquisitionCandidateVerification { <# .SYNOPSIS Builds the effective verification policy for one acquisition candidate. .DESCRIPTION Combines acquisition-candidate verification mode with canonical package-file content hash and publisher-signature metadata when present, while remaining compatible with candidate-local hash definitions. .PARAMETER Package The selected effective release. .PARAMETER AcquisitionCandidate The raw acquisition candidate. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Package, [Parameter(Mandatory = $true)] [psobject]$ArtifactFile, [AllowNull()] [psobject]$AcquisitionCandidate, [ValidateSet('off', 'warnWhenPackageFileExists', 'enforceWhenPackageFileExists', 'enforceAllAcquisition')] [string]$PayloadVerificationPolicy = 'off', [bool]$ArtifactFileRequired = $false ) $candidateVerification = if ($AcquisitionCandidate -and $AcquisitionCandidate.PSObject.Properties['verification']) { $AcquisitionCandidate.verification } else { $null } if ($candidateVerification -is [System.Collections.IDictionary]) { $candidateVerification = [pscustomobject]$candidateVerification } $packageContentHash = if ($ArtifactFile.PSObject.Properties['ContentHash']) { $ArtifactFile.ContentHash } else { $null } if ($packageContentHash -is [System.Collections.IDictionary]) { $packageContentHash = [pscustomobject]$packageContentHash } $packagePublisherSignature = if ($ArtifactFile.PSObject.Properties['PublisherSignature']) { $ArtifactFile.PublisherSignature } else { $null } if ($packagePublisherSignature -is [System.Collections.IDictionary]) { $packagePublisherSignature = [pscustomobject]$packagePublisherSignature } $packageFilePresent = -not [string]::IsNullOrWhiteSpace([string]$ArtifactFile.RelativePath) $packageContentHashPresent = $packageContentHash -and $packageContentHash.PSObject.Properties['value'] -and -not [string]::IsNullOrWhiteSpace([string]$packageContentHash.value) $packagePublisherSignaturePresent = $null -ne $packagePublisherSignature $packageBoundaryPresent = $packageContentHashPresent -or $packagePublisherSignaturePresent $payloadVerificationRequired = switch -Exact ($PayloadVerificationPolicy) { 'enforceWhenPackageFileExists' { [bool]($ArtifactFileRequired -and $packageFilePresent) } 'enforceAllAcquisition' { [bool]$ArtifactFileRequired } default { $false } } $payloadVerificationWarnOnly = [string]::Equals($PayloadVerificationPolicy, 'warnWhenPackageFileExists', [System.StringComparison]::OrdinalIgnoreCase) -and $ArtifactFileRequired -and $packageFilePresent -and -not $packageBoundaryPresent if ($payloadVerificationRequired -and -not $packageBoundaryPresent) { throw "Catalog payload policy '$PayloadVerificationPolicy' requires contentHash or publisherSignature for artifact file '$([string]$ArtifactFile.Id)' in package '$([string]$Package.id)'." } if ($payloadVerificationWarnOnly) { Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Catalog payload policy '{0}' found no trust boundary for artifact file '{1}' in package '{2}'." -f $PayloadVerificationPolicy, [string]$ArtifactFile.Id, [string]$Package.id) } $mode = if ($candidateVerification -and $candidateVerification.PSObject.Properties['mode'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.mode)) { [string]$candidateVerification.mode } else { 'none' } if ($payloadVerificationRequired) { $mode = 'required' } $algorithm = if ($packageContentHash -and $packageContentHash.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$packageContentHash.algorithm)) { ([string]$packageContentHash.algorithm).ToLowerInvariant() } elseif ($candidateVerification -and $candidateVerification.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.algorithm)) { ([string]$candidateVerification.algorithm).ToLowerInvariant() } else { 'sha256' } $hashValue = if ($packageContentHash -and $packageContentHash.PSObject.Properties['value'] -and -not [string]::IsNullOrWhiteSpace([string]$packageContentHash.value)) { [string]$packageContentHash.value } elseif ($candidateVerification -and $candidateVerification.PSObject.Properties[$algorithm] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.$algorithm)) { [string]$candidateVerification.$algorithm } elseif ($candidateVerification -and $candidateVerification.PSObject.Properties['sha256'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.sha256)) { [string]$candidateVerification.sha256 } else { $null } $verification = [ordered]@{ mode = $mode } if (-not [string]::IsNullOrWhiteSpace($algorithm)) { $verification.algorithm = $algorithm } if (-not [string]::IsNullOrWhiteSpace($hashValue)) { $verification[$algorithm] = $hashValue } if ($packagePublisherSignature) { $verification.authenticode = $packagePublisherSignature } return [pscustomobject]$verification } function Get-PackagePackageDepotSources { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageConfig ) $orderedSources = New-Object System.Collections.Generic.List[object] foreach ($property in @($PackageConfig.EnvironmentSources.PSObject.Properties)) { $source = $property.Value if (-not [string]::Equals([string]$source.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) { continue } if ($source.PSObject.Properties['readable'] -and -not [bool]$source.readable) { continue } $orderedSources.Add([pscustomobject]@{ id = $property.Name basePath = if ($source.PSObject.Properties['basePath']) { [string]$source.basePath } else { $null } searchOrder = if ($source.PSObject.Properties['searchOrder']) { [int]$source.searchOrder } else { 1000 } readable = if ($source.PSObject.Properties['readable']) { [bool]$source.readable } else { $true } writable = if ($source.PSObject.Properties['writable']) { [bool]$source.writable } else { $false } mirrorTarget = if ($source.PSObject.Properties['mirrorTarget']) { [bool]$source.mirrorTarget } else { $false } ensureExists = if ($source.PSObject.Properties['ensureExists']) { [bool]$source.ensureExists } else { $false } }) | Out-Null } return @( $orderedSources.ToArray() | Sort-Object -Property searchOrder, id ) } function Get-PackageDepotDistributionTargets { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageConfig ) $targets = New-Object System.Collections.Generic.List[object] foreach ($property in @($PackageConfig.EnvironmentSources.PSObject.Properties)) { $source = $property.Value if (-not ($source.PSObject.Properties['writable'] -and [bool]$source.writable)) { continue } if (-not ($source.PSObject.Properties['mirrorTarget'] -and [bool]$source.mirrorTarget)) { continue } $targets.Add([pscustomobject]@{ id = $property.Name kind = if ($source.PSObject.Properties['kind']) { [string]$source.kind } else { $null } basePath = if ($source.PSObject.Properties['basePath']) { [string]$source.basePath } else { $null } searchOrder = if ($source.PSObject.Properties['searchOrder']) { [int]$source.searchOrder } else { 1000 } ensureExists = if ($source.PSObject.Properties['ensureExists']) { [bool]$source.ensureExists } else { $false } }) | Out-Null } return @( $targets.ToArray() | Sort-Object -Property searchOrder, id ) } function Get-PackageDepotDistributionFileHash { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path ) return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } function Test-PackageDepotDistributionFileMatches { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourcePath, [Parameter(Mandatory = $true)] [string]$TargetPath ) if (-not (Test-Path -LiteralPath $TargetPath -PathType Leaf)) { return [pscustomobject]@{ Matches = $false Reason = 'Missing' } } $sourceItem = Get-Item -LiteralPath $SourcePath -ErrorAction Stop $targetItem = Get-Item -LiteralPath $TargetPath -ErrorAction Stop if ($sourceItem.Length -ne $targetItem.Length) { return [pscustomobject]@{ Matches = $false Reason = 'SizeMismatch' } } $sourceHash = Get-PackageDepotDistributionFileHash -Path $SourcePath $targetHash = Get-PackageDepotDistributionFileHash -Path $TargetPath return [pscustomobject]@{ Matches = [string]::Equals($sourceHash, $targetHash, [System.StringComparison]::OrdinalIgnoreCase) Reason = if ([string]::Equals($sourceHash, $targetHash, [System.StringComparison]::OrdinalIgnoreCase)) { 'AlreadyCurrent' } else { 'HashMismatch' } } } function Get-PackageArtifactFileResult { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [string]$ArtifactFileId ) return @($PackageResult.ArtifactFiles | Where-Object { [string]::Equals([string]$_.Id, $ArtifactFileId, [System.StringComparison]::OrdinalIgnoreCase) }) | Select-Object -First 1 } function Build-PackageAcquisitionPlan { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $package = $PackageResult.Package if (-not $package) { throw 'Build-PackageAcquisitionPlan requires a selected release.' } $offline = $PackageResult.PSObject.Properties['Offline'] -and [bool]$PackageResult.Offline $payloadVerificationPolicy = if ($PackageResult.PackageConfig -and $PackageResult.PackageConfig.PSObject.Properties['CatalogTrustPayloadVerification'] -and -not [string]::IsNullOrWhiteSpace([string]$PackageResult.PackageConfig.CatalogTrustPayloadVerification)) { [string]$PackageResult.PackageConfig.CatalogTrustPayloadVerification } else { 'off' } $filePlans = New-Object System.Collections.Generic.List[object] $skippedOfflineCandidateCount = 0 foreach ($artifactFile in @($PackageResult.ArtifactFiles)) { $orderedCandidates = New-Object System.Collections.Generic.List[object] foreach ($candidate in @($artifactFile.AcquisitionCandidates | Sort-Object -Property @{ Expression = { if ($_.PSObject.Properties['searchOrder']) { [int]$_.searchOrder } else { [int]::MaxValue } } })) { $candidateKind = [string]$candidate.kind if ($offline -and $candidateKind -notin @('packageDepot', 'archiveEntry')) { $skippedOfflineCandidateCount++ continue } $resolvedVerification = Resolve-PackageAcquisitionCandidateVerification ` -Package $package ` -ArtifactFile $artifactFile ` -AcquisitionCandidate $candidate ` -PayloadVerificationPolicy $payloadVerificationPolicy ` -ArtifactFileRequired $true $searchOrder = if ($candidate.PSObject.Properties['searchOrder']) { [int]$candidate.searchOrder } else { [int]::MaxValue } switch -Exact ($candidateKind) { 'packageDepot' { $sourcePath = Join-Path $PackageResult.PackageDepotRelativeDirectory ([string]$artifactFile.RelativePath) foreach ($depotSource in @(Get-PackagePackageDepotSources -PackageConfig $PackageResult.PackageConfig)) { $orderedCandidates.Add([pscustomobject]@{ kind = 'packageDepot' searchOrder = $searchOrder sourceSearchOrder = [int]$depotSource.searchOrder sourceRef = [pscustomobject]@{ scope = 'environment'; id = [string]$depotSource.id } sourcePath = $sourcePath verification = $resolvedVerification }) | Out-Null } } 'vendorDownload' { $orderedCandidates.Add([pscustomobject]@{ kind = 'vendorDownload' searchOrder = $searchOrder sourceSearchOrder = 1000 sourceRef = if ($candidate.PSObject.Properties['sourceId'] -and -not [string]::IsNullOrWhiteSpace([string]$candidate.sourceId)) { [pscustomobject]@{ scope = 'definition'; id = [string]$candidate.sourceId } } else { $null } sourcePath = if ($candidate.PSObject.Properties['sourcePath']) { [string]$candidate.sourcePath } else { $null } url = if ($candidate.PSObject.Properties['url']) { [string]$candidate.url } else { $null } verification = $resolvedVerification }) | Out-Null } 'archiveEntry' { $orderedCandidates.Add([pscustomobject]@{ kind = 'archiveEntry' searchOrder = $searchOrder sourceSearchOrder = 1000 sourceArtifactFileId = [string]$candidate.sourceArtifactFileId entryPath = [string]$candidate.entryPath verification = $resolvedVerification }) | Out-Null } default { throw "Unsupported artifact acquisition candidate kind '$candidateKind'." } } } $plan = [pscustomobject]@{ ArtifactFileId = [string]$artifactFile.Id StagingPath = [string]$artifactFile.StagingPath DefaultDepotPath = [string]$artifactFile.DefaultDepotPath Offline = $offline Candidates = @($orderedCandidates.ToArray() | Sort-Object -Property searchOrder, sourceSearchOrder, @{ Expression = { if ($_.sourceRef) { [string]$_.sourceRef.id } else { [string]::Empty } } }) } $artifactFile.AcquisitionPlan = $plan $filePlans.Add($plan) | Out-Null } $PackageResult.ArtifactAcquisitionPlan = [pscustomobject]@{ ArtifactFilesRequired = @($PackageResult.ArtifactFiles).Count -gt 0 ArtifactStagingDirectory = [string]$PackageResult.ArtifactStagingDirectory Offline = $offline SkippedOfflineCandidateCount = $skippedOfflineCandidateCount Files = @($filePlans.ToArray()) } Write-PackageExecutionMessage -Message ("[STATE] Acquisition plans built for {0} required artifact file(s); offline='{1}'." -f $filePlans.Count, $offline) return $PackageResult } function Expand-PackageDeclaredArchiveEntry { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ArchivePath, [Parameter(Mandatory = $true)] [string]$EntryPath, [Parameter(Mandatory = $true)] [string]$TargetPath ) $normalizedEntryPath = $EntryPath.Replace('\', '/').TrimStart('/') if ([string]::IsNullOrWhiteSpace($normalizedEntryPath) -or [System.IO.Path]::IsPathRooted($EntryPath) -or @($normalizedEntryPath.Split('/') | Where-Object { $_ -in @('', '.', '..') }).Count -gt 0) { throw "Archive entry path '$EntryPath' is unsafe." } Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue $archive = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath) try { $matches = @($archive.Entries | Where-Object { [string]::Equals($_.FullName.Replace('\', '/').TrimStart('/'), $normalizedEntryPath, [System.StringComparison]::OrdinalIgnoreCase) }) if ($matches.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string]$matches[0].Name)) { throw "Archive '$ArchivePath' must contain exactly one file entry named '$EntryPath'; found $($matches.Count)." } $targetDirectory = Split-Path -Parent $TargetPath $null = New-Item -ItemType Directory -Path $targetDirectory -Force $inputStream = $matches[0].Open() try { $outputStream = [System.IO.File]::Open($TargetPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None) try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose() } } finally { $inputStream.Dispose() } } finally { $archive.Dispose() } return (Resolve-Path -LiteralPath $TargetPath -ErrorAction Stop).Path } function Resolve-PackageArtifactFile { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult, [Parameter(Mandatory = $true)] [psobject]$ArtifactFile, [Parameter(Mandatory = $true)] [hashtable]$ResolutionState, [Parameter(Mandatory = $true)] [hashtable]$Visiting ) $artifactFileId = [string]$ArtifactFile.Id if ($ResolutionState.ContainsKey($artifactFileId)) { return [bool]$ResolutionState[$artifactFileId] } if ($Visiting.ContainsKey($artifactFileId)) { throw "Artifact acquisition cycle encountered while resolving '$artifactFileId'." } $Visiting[$artifactFileId] = $true $attempts = New-Object System.Collections.Generic.List[object] $orderedCandidates = @($ArtifactFile.AcquisitionPlan.Candidates) $preferredVerification = Get-PackagePreferredVerification -AcquisitionCandidates $orderedCandidates $offline = $PackageResult.PSObject.Properties['Offline'] -and [bool]$PackageResult.Offline if ((-not $offline) -and (Test-Path -LiteralPath $ArtifactFile.StagingPath -PathType Leaf)) { $verification = Test-PackageSavedFile -Path $ArtifactFile.StagingPath -Verification $preferredVerification $attempts.Add([pscustomobject]@{ AttemptType = 'ReuseCheck'; Status = if ($verification.Accepted) { 'ReusedArtifactFile' } else { 'ReuseRejected' } SourceScope = 'artifactStaging'; SourceId = $artifactFileId; SourceKind = 'filesystem' ResolvedSource = [string]$ArtifactFile.StagingPath; VerificationStatus = $verification.Status ErrorMessage = if ($verification.Accepted) { $null } else { 'Existing staged artifact did not satisfy verification.' } }) | Out-Null if ($verification.Accepted) { $ArtifactFile.Verification = $verification $ArtifactFile.Preparation = [pscustomobject]@{ Success = $true; Status = 'ReusedArtifactFile'; ArtifactFileId = $artifactFileId StagingPath = [string]$ArtifactFile.StagingPath; SelectedSource = [pscustomobject]@{ SourceScope = 'artifactStaging'; SourceId = $artifactFileId; SourceKind = 'filesystem'; ResolvedSource = [string]$ArtifactFile.StagingPath } Verification = $verification; Attempts = @($attempts.ToArray()); FailureReason = $null; ErrorMessage = $null } $ResolutionState[$artifactFileId] = $true $Visiting.Remove($artifactFileId) return $true } } $targetDirectory = Split-Path -Parent ([string]$ArtifactFile.StagingPath) $null = New-Item -ItemType Directory -Path $targetDirectory -Force foreach ($candidate in $orderedCandidates) { $sourceDefinition = $null $resolvedSource = $null $verification = $null $partialPath = '{0}.{1}.partial' -f $ArtifactFile.StagingPath, ([guid]::NewGuid().ToString('N')) try { if ([string]::Equals([string]$candidate.kind, 'archiveEntry', [System.StringComparison]::OrdinalIgnoreCase)) { $sourceArtifactFile = Get-PackageArtifactFileResult -PackageResult $PackageResult -ArtifactFileId ([string]$candidate.sourceArtifactFileId) if (-not $sourceArtifactFile) { throw "Artifact file '$artifactFileId' references missing source artifact file '$($candidate.sourceArtifactFileId)'." } if (-not (Resolve-PackageArtifactFile -PackageResult $PackageResult -ArtifactFile $sourceArtifactFile -ResolutionState $ResolutionState -Visiting $Visiting)) { throw "Source artifact file '$($candidate.sourceArtifactFileId)' could not be prepared." } $sourceDefinition = [pscustomobject]@{ Scope = 'artifact'; Id = [string]$sourceArtifactFile.Id; Kind = 'archiveEntry' } $resolvedSource = [pscustomobject]@{ Kind = 'archiveEntry'; ResolvedSource = [string]$sourceArtifactFile.StagingPath } $null = Expand-PackageDeclaredArchiveEntry -ArchivePath $sourceArtifactFile.StagingPath -EntryPath ([string]$candidate.entryPath) -TargetPath $partialPath } else { if ($candidate.sourceRef) { $sourceDefinition = Get-PackageSourceDefinition -PackageConfig $PackageResult.PackageConfig -SourceRef $candidate.sourceRef } elseif ([string]::Equals([string]$candidate.kind, 'vendorDownload', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::IsNullOrWhiteSpace([string]$candidate.url)) { $sourceDefinition = [pscustomobject]@{ Scope = 'direct'; Id = 'directDownload'; Kind = 'download'; BaseUri = $null; BasePath = $null GitHubOwner = $null; GitHubRepository = $null } } else { throw "Artifact acquisition candidate kind '$($candidate.kind)' could not be resolved to a source definition." } $resolvedSource = Resolve-PackageSource -SourceDefinition $sourceDefinition -AcquisitionCandidate $candidate -Package $PackageResult.Package -ArtifactFile $ArtifactFile switch -Exact ([string]$resolvedSource.Kind) { 'download' { $null = Save-PackageDownloadFile -Uri $resolvedSource.ResolvedSource -TargetPath $partialPath } 'filesystem' { $null = Save-PackageFilesystemFile -SourcePath $resolvedSource.ResolvedSource -TargetPath $partialPath } default { throw "Unsupported artifact source kind '$($resolvedSource.Kind)'." } } } $verification = Test-PackageSavedFile -Path $partialPath -Verification $candidate.verification if (-not $verification.Accepted) { throw "Prepared artifact file '$artifactFileId' did not satisfy verification ($($verification.Status))." } if (Test-Path -LiteralPath $ArtifactFile.StagingPath) { Remove-Item -LiteralPath $ArtifactFile.StagingPath -Force } Move-Item -LiteralPath $partialPath -Destination $ArtifactFile.StagingPath -Force $selectedSource = [pscustomobject]@{ SourceScope = [string]$sourceDefinition.Scope; SourceId = [string]$sourceDefinition.Id SourceKind = [string]$resolvedSource.Kind; ResolvedSource = [string]$resolvedSource.ResolvedSource } $preparedStatus = if ($candidate.kind -eq 'archiveEntry') { 'ExtractedArchiveEntry' } elseif ($candidate.kind -eq 'packageDepot' -and $sourceDefinition.Id -eq 'defaultPackageDepot') { 'HydratedFromDefaultPackageDepot' } elseif ($candidate.kind -eq 'packageDepot') { 'HydratedFromPackageDepot' } else { 'SavedArtifactFile' } $attempts.Add([pscustomobject]@{ AttemptType = 'Save'; Status = $preparedStatus SourceScope = $selectedSource.SourceScope; SourceId = $selectedSource.SourceId; SourceKind = $selectedSource.SourceKind ResolvedSource = $selectedSource.ResolvedSource; VerificationStatus = $verification.Status; ErrorMessage = $null }) | Out-Null $ArtifactFile.Verification = $verification $ArtifactFile.Preparation = [pscustomobject]@{ Success = $true; Status = $attempts[$attempts.Count - 1].Status; ArtifactFileId = $artifactFileId StagingPath = [string]$ArtifactFile.StagingPath; SelectedSource = $selectedSource; Verification = $verification Attempts = @($attempts.ToArray()); FailureReason = $null; ErrorMessage = $null } $ResolutionState[$artifactFileId] = $true $Visiting.Remove($artifactFileId) Write-PackageExecutionMessage -Message ("[ACTION] Prepared artifact file '{0}' from '{1}:{2}'." -f $artifactFileId, $selectedSource.SourceScope, $selectedSource.SourceId) return $true } catch { if (Test-Path -LiteralPath $partialPath) { Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue } $attempts.Add([pscustomobject]@{ AttemptType = 'Save'; Status = 'Failed' SourceScope = if ($sourceDefinition) { [string]$sourceDefinition.Scope } elseif ($candidate.sourceRef) { [string]$candidate.sourceRef.scope } else { $null } SourceId = if ($sourceDefinition) { [string]$sourceDefinition.Id } elseif ($candidate.sourceRef) { [string]$candidate.sourceRef.id } else { $null } SourceKind = if ($resolvedSource) { [string]$resolvedSource.Kind } else { [string]$candidate.kind } ResolvedSource = if ($resolvedSource) { [string]$resolvedSource.ResolvedSource } else { $null } VerificationStatus = if ($verification) { [string]$verification.Status } else { $null } ErrorMessage = $_.Exception.Message }) | Out-Null if (-not $PackageResult.PackageConfig.AllowAcquisitionFallback) { break } } } $failureReason = if ($offline) { 'DepotMiss' } else { 'AllSourcesFailed' } $ArtifactFile.Preparation = [pscustomobject]@{ Success = $false; Status = 'Failed'; ArtifactFileId = $artifactFileId; StagingPath = [string]$ArtifactFile.StagingPath SelectedSource = $null; Verification = $null; Attempts = @($attempts.ToArray()); FailureReason = $failureReason ErrorMessage = "No verified acquisition candidate produced required artifact file '$artifactFileId'." } $ResolutionState[$artifactFileId] = $false $Visiting.Remove($artifactFileId) return $false } function Resolve-PackageArtifactFiles { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) if (-not $PackageResult.ArtifactAcquisitionPlan) { $PackageResult = Build-PackageAcquisitionPlan -PackageResult $PackageResult } if (@($PackageResult.ArtifactFiles).Count -eq 0) { $PackageResult.ArtifactPreparation = [pscustomobject]@{ Success = $true; Status = 'Skipped'; Files = @(); MissingArtifactFiles = @() } return $PackageResult } $state = @{} foreach ($artifactFile in @($PackageResult.ArtifactFiles)) { $null = Resolve-PackageArtifactFile -PackageResult $PackageResult -ArtifactFile $artifactFile -ResolutionState $state -Visiting @{} } $missing = @($PackageResult.ArtifactFiles | Where-Object { -not $_.Preparation -or -not [bool]$_.Preparation.Success }) $PackageResult.ArtifactPreparation = [pscustomobject]@{ Success = $missing.Count -eq 0 Status = if ($missing.Count -eq 0) { 'Prepared' } else { 'Failed' } Files = @($PackageResult.ArtifactFiles | ForEach-Object { $_.Preparation }) MissingArtifactFiles = @($missing | ForEach-Object { [pscustomobject]@{ Id = [string]$_.Id; RelativePath = [string]$_.RelativePath; ExpectedDepotPath = [string]$_.DefaultDepotPath } }) } if ($missing.Count -gt 0) { $details = @($missing | ForEach-Object { "'$($_.Id)' at '$($_.DefaultDepotPath)'" }) -join ', ' throw "Required artifact file acquisition failed for package '$($PackageResult.Package.id)': $details." } return $PackageResult } function Resolve-PackageDepotDistributionPlan { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $mode = if ($PackageResult.PackageConfig.PSObject.Properties['DepotDistributionMode'] -and -not [string]::IsNullOrWhiteSpace([string]$PackageResult.PackageConfig.DepotDistributionMode)) { [string]$PackageResult.PackageConfig.DepotDistributionMode } else { 'packageFocused' } $actions = New-Object System.Collections.Generic.List[object] $result = [pscustomobject]@{ Mode = $mode; Status = 'Skipped'; Reason = $null; Actions = @() } if ([string]::Equals($mode, 'disabled', [System.StringComparison]::OrdinalIgnoreCase)) { $result.Reason = 'DisabledByPolicy' return $result } if (@($PackageResult.ArtifactFiles).Count -eq 0) { $result.Reason = 'NoArtifactFiles' return $result } if (-not $PackageResult.ArtifactPreparation -or -not [bool]$PackageResult.ArtifactPreparation.Success) { throw 'Depot distribution requires the complete verified artifact file set.' } $targets = @(Get-PackageDepotDistributionTargets -PackageConfig $PackageResult.PackageConfig) if ($targets.Count -eq 0) { $result.Reason = 'NoWritableMirrorTargets' return $result } foreach ($artifactFile in @($PackageResult.ArtifactFiles)) { if (-not (Test-Path -LiteralPath $artifactFile.StagingPath -PathType Leaf)) { throw "Verified staging file for artifact '$($artifactFile.Id)' is missing." } foreach ($target in $targets) { if (-not [string]::Equals([string]$target.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase) -or [string]::IsNullOrWhiteSpace([string]$target.basePath)) { continue } $targetPackageDirectory = Resolve-PackageArtifactChildPath -RootPath ([string]$target.basePath) -RelativePath ([string]$PackageResult.PackageDepotRelativeDirectory) -ArtifactFileId ([string]$artifactFile.Id) $targetPath = Resolve-PackageArtifactChildPath -RootPath $targetPackageDirectory -RelativePath ([string]$artifactFile.RelativePath) -ArtifactFileId ([string]$artifactFile.Id) $comparison = Test-PackageDepotDistributionFileMatches -SourcePath ([string]$artifactFile.StagingPath) -TargetPath $targetPath $actions.Add([pscustomobject]@{ ArtifactFileId = [string]$artifactFile.Id; DepotId = [string]$target.id SourcePath = [string]$artifactFile.StagingPath; TargetPath = $targetPath Action = if ($comparison.Matches) { 'Skip' } else { 'Copy' } Status = if ($comparison.Matches) { 'Skipped' } else { 'Pending' } Reason = [string]$comparison.Reason; EnsureExists = [bool]$target.ensureExists; ErrorMessage = $null }) | Out-Null } } $result.Status = 'Planned' $result.Actions = @($actions.ToArray()) return $result } function Invoke-PackageDepotDistribution { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$PackageResult ) $plan = Resolve-PackageDepotDistributionPlan -PackageResult $PackageResult foreach ($action in @($plan.Actions)) { if ($action.Action -ne 'Copy') { continue } try { $targetDirectory = Split-Path -Parent ([string]$action.TargetPath) $null = New-Item -ItemType Directory -Path $targetDirectory -Force $null = Copy-FileToPath -SourcePath ([string]$action.SourcePath) -TargetPath ([string]$action.TargetPath) -Overwrite $action.Status = 'Copied' Write-PackageExecutionMessage -Message ("[ACTION] Mirrored artifact file '{0}' to depot '{1}'." -f $action.ArtifactFileId, $action.DepotId) } catch { $action.Status = 'Failed' $action.ErrorMessage = $_.Exception.Message Write-PackageExecutionMessage -Level 'WRN' -Message ("[WARN] Failed to mirror artifact file '{0}' to depot '{1}': {2}" -f $action.ArtifactFileId, $action.DepotId, $_.Exception.Message) } } $plan | Add-Member -MemberType NoteProperty -Name CopiedCount -Value @($plan.Actions | Where-Object Status -eq 'Copied').Count -Force $plan | Add-Member -MemberType NoteProperty -Name FailedCount -Value @($plan.Actions | Where-Object Status -eq 'Failed').Count -Force $plan | Add-Member -MemberType NoteProperty -Name SkippedCount -Value @($plan.Actions | Where-Object Status -eq 'Skipped').Count -Force $PackageResult.DepotDistribution = $plan return $PackageResult } |