diff --git a/.azure-pipelines/1es-entra-powershell-ci-build.yml b/.azure-pipelines/1es-entra-powershell-ci-build.yml index 5a9bb1cf33..63c888377d 100644 --- a/.azure-pipelines/1es-entra-powershell-ci-build.yml +++ b/.azure-pipelines/1es-entra-powershell-ci-build.yml @@ -55,6 +55,11 @@ extends: - template: .azure-pipelines/generation-templates/generate_adapter-1es.yml@self parameters: Sign: ${{ parameters.Sign }} + # 1ES network isolation (CFSClean/CFSClean2) blocks the public + # PowerShell Gallery. Install dependencies from the CFS feed instead. + DependencyRepository: 'EntraCFS' + # TODO: replace with the onboarded CFS feed v2 index URL (see https://aka.ms/cfs). + CfsFeedUrl: 'https://pkgs.dev.azure.com/msazure/One/_packaging/REPLACE_WITH_CFS_FEED/nuget/v2' - ${{ if and(eq(parameters.Pack, true), eq(parameters.Sign, true)) }}: - template: .azure-pipelines/common-templates/esrp/codesign-nuget.yml@self diff --git a/.azure-pipelines/1es-entra-powershell-pr.yml b/.azure-pipelines/1es-entra-powershell-pr.yml index 2c80228079..5061730768 100644 --- a/.azure-pipelines/1es-entra-powershell-pr.yml +++ b/.azure-pipelines/1es-entra-powershell-pr.yml @@ -47,4 +47,9 @@ extends: - template: .azure-pipelines/generation-templates/generate_adapter-1es.yml@self parameters: Sign: false + # 1ES network isolation (CFSClean/CFSClean2) blocks the public + # PowerShell Gallery. Install dependencies from the CFS feed instead. + DependencyRepository: 'EntraCFS' + # TODO: replace with the onboarded CFS feed v2 index URL (see https://aka.ms/cfs). + CfsFeedUrl: 'https://pkgs.dev.azure.com/msazure/One/_packaging/REPLACE_WITH_CFS_FEED/nuget/v2' \ No newline at end of file diff --git a/.azure-pipelines/generation-templates/generate_adapter-1es.yml b/.azure-pipelines/generation-templates/generate_adapter-1es.yml index e146f9ab11..a1a36fc7fe 100644 --- a/.azure-pipelines/generation-templates/generate_adapter-1es.yml +++ b/.azure-pipelines/generation-templates/generate_adapter-1es.yml @@ -9,8 +9,21 @@ parameters: - name: Integration type: boolean default: false + # Name of the PowerShell repository build/test dependencies are installed from. + # Defaults to the public PowerShell Gallery; 1ES pipelines pass a CFS-backed + # feed to satisfy network-isolation (CFSClean) policies. + - name: DependencyRepository + type: string + default: 'PSGallery' + # v2 index URL of the CFS feed backing DependencyRepository. + - name: CfsFeedUrl + type: string + default: '' steps: +- ${{ if ne(parameters.CfsFeedUrl, '') }}: + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to CFS feed' - task: powershell@2 displayName: 'Show current PowerShell version information' inputs: @@ -30,12 +43,20 @@ steps: script: | ./build/Install-Dependencies.ps1 -ModuleName Entra -Verbose pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Install PlatyPS' inputs: targetType: inline - script: Install-Module PlatyPS -scope currentuser -Force + script: ./build/Install-GalleryModule.ps1 -Name PlatyPS pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Build Entra' inputs: @@ -102,6 +123,10 @@ steps: script: | ./build/Install-Dependencies.ps1 -ModuleName EntraBeta -Verbose pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Build EntraBeta' inputs: @@ -157,8 +182,12 @@ steps: displayName: 'Install Pester' inputs: targetType: inline - script: Install-Module Pester -scope currentuser -SkipPublisherCheck -Force + script: ./build/Install-GalleryModule.ps1 -Name Pester -SkipPublisherCheck pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Run tests Entra' inputs: diff --git a/.azure-pipelines/generation-templates/generate_adapter.yml b/.azure-pipelines/generation-templates/generate_adapter.yml index 2cc880bf12..7207a22148 100644 --- a/.azure-pipelines/generation-templates/generate_adapter.yml +++ b/.azure-pipelines/generation-templates/generate_adapter.yml @@ -9,8 +9,21 @@ parameters: - name: Integration type: boolean default: false + # Name of the PowerShell repository build/test dependencies are installed from. + # Defaults to the public PowerShell Gallery; pipelines pass a CFS-backed feed + # to satisfy network-isolation (CFSClean) policies. + - name: DependencyRepository + type: string + default: 'PSGallery' + # v2 index URL of the CFS feed backing DependencyRepository. + - name: CfsFeedUrl + type: string + default: '' steps: +- ${{ if ne(parameters.CfsFeedUrl, '') }}: + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to CFS feed' - task: powershell@2 displayName: 'Show current PowerShell version information' inputs: @@ -30,12 +43,20 @@ steps: script: | ./build/Install-Dependencies.ps1 -ModuleName Entra -Verbose pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Install PlatyPS' inputs: targetType: inline - script: Install-Module PlatyPS -scope currentuser -Force + script: ./build/Install-GalleryModule.ps1 -Name PlatyPS pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Build Entra' inputs: @@ -106,6 +127,10 @@ steps: script: | ./build/Install-Dependencies.ps1 -ModuleName EntraBeta -Verbose pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Build EntraBeta' inputs: @@ -165,8 +190,12 @@ steps: displayName: 'Install Pester' inputs: targetType: inline - script: Install-Module Pester -scope currentuser -SkipPublisherCheck -Force + script: ./build/Install-GalleryModule.ps1 -Name Pester -SkipPublisherCheck pwsh: true + env: + DEPENDENCY_PS_REPO: ${{ parameters.DependencyRepository }} + DEPENDENCY_PS_FEED_URL: ${{ parameters.CfsFeedUrl }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - task: powershell@2 displayName: 'Run tests Entra' inputs: diff --git a/.azure-pipelines/integration-tests.yml b/.azure-pipelines/integration-tests.yml index 3046b03caa..b266c52412 100644 --- a/.azure-pipelines/integration-tests.yml +++ b/.azure-pipelines/integration-tests.yml @@ -26,4 +26,9 @@ stages: - template: ./generation-templates/generate_adapter.yml parameters: Integration: true - Sign: false \ No newline at end of file + Sign: false + # Install dependencies from the CFS feed to stay consistent with the + # 1ES CI/PR pipelines (CFSClean/CFSClean2 network-isolation policies). + DependencyRepository: 'EntraCFS' + # TODO: replace with the onboarded CFS feed v2 index URL (see https://aka.ms/cfs). + CfsFeedUrl: 'https://pkgs.dev.azure.com/msazure/One/_packaging/REPLACE_WITH_CFS_FEED/nuget/v2' \ No newline at end of file diff --git a/build/Install-Dependencies.ps1 b/build/Install-Dependencies.ps1 index decedaa745..2792d45dd0 100644 --- a/build/Install-Dependencies.ps1 +++ b/build/Install-Dependencies.ps1 @@ -10,7 +10,14 @@ param( [ValidateScript({ Test-Path $_ })] [string] - $ModuleSettingsPath + $ModuleSettingsPath, + + # PowerShell repository to install dependencies from. Defaults to the public + # PowerShell Gallery for local development. CI pipelines running under 1ES + # network isolation set DEPENDENCY_PS_REPO to a CFS-backed feed to satisfy the + # CFSClean/CFSClean2 policies. See build/Install-GalleryModule.ps1. + [string] + $Repository = $(if ($env:DEPENDENCY_PS_REPO) { $env:DEPENDENCY_PS_REPO } else { 'PSGallery' }) ) . "$psscriptroot/common-functions.ps1" @@ -26,5 +33,5 @@ Write-Verbose("Skipping deprecated source module '$($content.sourceModule)' - no foreach ($moduleName in $content.destinationModuleName){ Write-Verbose("Installing Module $($moduleName)") - Install-Module $moduleName -scope currentuser -RequiredVersion $content.destinationModuleVersion -Force -AllowClobber + & "$PSScriptRoot/Install-GalleryModule.ps1" -Name $moduleName -RequiredVersion $content.destinationModuleVersion -Repository $Repository -AllowClobber -Verbose:($VerbosePreference -eq 'Continue') } \ No newline at end of file diff --git a/build/Install-GalleryModule.ps1 b/build/Install-GalleryModule.ps1 new file mode 100644 index 0000000000..b14fe87c0d --- /dev/null +++ b/build/Install-GalleryModule.ps1 @@ -0,0 +1,119 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ + +<# +.SYNOPSIS + Installs PowerShell modules used by the build, routing the download through a + CFS (Centralized Feed Service) feed when one is configured. + +.DESCRIPTION + The 1ES CI/PR pipelines run under network isolation. The CFSClean/CFSClean2 + policies block access to the public PowerShell Gallery (powershellgallery.com) + and require every package to be restored from a CFS-backed Azure Artifacts + feed instead. See build/BUILD.md and https://aka.ms/1es/netiso/pipelinetemplates. + + This helper is the single place where build/test dependencies are installed so + that the CFS wiring lives in one location: + + * Local development uses the public PowerShell Gallery (the default), so no + extra configuration is required. + * CI pipelines set the DEPENDENCY_PS_REPO and DEPENDENCY_PS_FEED_URL + environment variables (and expose SYSTEM_ACCESSTOKEN) so the same modules + are restored from the CFS feed. + + If a non-PSGallery repository is requested but no feed URL has been supplied + (for example while the CFS feed URL is still a placeholder in a draft PR), the + helper warns and falls back to the public PowerShell Gallery instead of + failing the build. + +.PARAMETER Name + One or more module names to install. + +.PARAMETER RequiredVersion + Optional exact version to install. + +.PARAMETER Repository + Name of the PowerShell repository to install from. Defaults to the value of + the DEPENDENCY_PS_REPO environment variable, or 'PSGallery' when it is unset. + +.PARAMETER FeedUrl + The v2 index URL of the CFS feed backing -Repository. Defaults to the + DEPENDENCY_PS_FEED_URL environment variable. Only used when -Repository is not + 'PSGallery'. + +.PARAMETER Token + Access token used to authenticate to the CFS feed. Defaults to the + SYSTEM_ACCESSTOKEN environment variable (mapped from $(System.AccessToken) in + the pipeline). +#> +[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The pipeline access token is only available as plain text and must be converted to a SecureString to build a PSCredential for the CFS feed.')] +param( + [Parameter(Mandatory)] + [string[]] $Name, + + [string] $RequiredVersion, + + [string] $Repository = $(if ($env:DEPENDENCY_PS_REPO) { $env:DEPENDENCY_PS_REPO } else { 'PSGallery' }), + + [string] $FeedUrl = $env:DEPENDENCY_PS_FEED_URL, + + [string] $Token = $env:SYSTEM_ACCESSTOKEN, + + [switch] $SkipPublisherCheck, + + [switch] $AllowClobber +) + +$ErrorActionPreference = 'Stop' + +# Treat a not-yet-onboarded placeholder feed URL as "no feed configured". +$feedIsPlaceholder = [string]::IsNullOrWhiteSpace($FeedUrl) -or $FeedUrl -match 'REPLACE_WITH|<.*>' + +if ($Repository -ne 'PSGallery' -and $feedIsPlaceholder) { + Write-Warning "Repository '$Repository' was requested but no CFS feed URL is configured (DEPENDENCY_PS_FEED_URL). Falling back to the public PowerShell Gallery. Set the CFS feed URL to satisfy 1ES network-isolation (CFSClean) policies." + $Repository = 'PSGallery' +} + +$credential = $null + +if ($Repository -ne 'PSGallery') { + if ($Token) { + $securePat = ConvertTo-SecureString $Token -AsPlainText -Force + $credential = [System.Management.Automation.PSCredential]::new('cfs', $securePat) + } + else { + Write-Warning "No access token available (SYSTEM_ACCESSTOKEN); authentication to '$Repository' may fail." + } + + # Register the CFS feed for PowerShellGet (idempotent - registration persists + # across pipeline steps, but the credential does not, so it is always supplied + # again at install time below). + if (-not (Get-PSRepository -Name $Repository -ErrorAction SilentlyContinue)) { + Write-Verbose "Registering PSRepository '$Repository' -> $FeedUrl" + $register = @{ + Name = $Repository + SourceLocation = $FeedUrl + InstallationPolicy = 'Trusted' + } + if ($credential) { $register['Credential'] = $credential } + Register-PSRepository @register + } +} + +foreach ($module in $Name) { + Write-Verbose "Installing module '$module' from repository '$Repository'" + $install = @{ + Name = $module + Repository = $Repository + Scope = 'CurrentUser' + Force = $true + } + if ($RequiredVersion) { $install['RequiredVersion'] = $RequiredVersion } + if ($SkipPublisherCheck) { $install['SkipPublisherCheck'] = $true } + if ($AllowClobber) { $install['AllowClobber'] = $true } + if ($credential) { $install['Credential'] = $credential } + + Install-Module @install +}