diff --git a/.github/workflows/publish_package.yaml b/.github/workflows/publish_package.yaml deleted file mode 100644 index e69de29bb..000000000 diff --git a/.github/workflows/publish_private_package.yaml b/.github/workflows/publish_private_package.yaml new file mode 100644 index 000000000..1cd304fb6 --- /dev/null +++ b/.github/workflows/publish_private_package.yaml @@ -0,0 +1,99 @@ +# Purpose: Publish nightly to a a private gallery just to make +# sure that the code can be published. This is like a +# smoke test. + +name: Publish Private Package + +on: + schedule: + - cron: "23 0 * * *" # Execute each day at 00:23 UTC + workflow_dispatch: + # for testing + # push: + # paths: + # - ".github/workflows/publish_private_package.yaml" + # - "utils/DeployUtils.ps1" + +env: + GalleryName: PrivateScubaGearGallery + +jobs: + publish: + name: Publish to Private Gallery + runs-on: windows-latest + environment: Development + permissions: + id-token: write + contents: write + defaults: + run: + shell: powershell + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: repo + - name: Install Azure Signing Tool + run: | + dotnet --version + dotnet tool install --global AzureSignTool --version 4.0.1 + # OIDC Login to Azure Public Cloud with AzPowershell (enableAzPSSession true) + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Create Private Gallery + run: | + cd repo + . utils/DeployUtils.ps1 + New-PrivateGallery -GalleryName $env:GalleryName -Trusted + - name: Get Key Vault Info + id: key-vault-info + env: + KEY_VAULT_INFO: ${{ secrets.SCUBA_KEY_VAULT_PROD}} + run: | + $KeyVaultInfo = ${env:KEY_VAULT_INFO} | ConvertFrom-Json + echo "KeyVaultUrl=$($KeyVaultInfo.KeyVault.URL)" >> $env:GITHUB_OUTPUT + echo "KeyVaultCertificateName=$($KeyVaultInfo.KeyVault.CertificateName)" >> $env:GITHUB_OUTPUT + - name: Sign and Publish Module + uses: azure/powershell@v1 + with: + inlineScript: | + # Source the deploy utilities so the functions in it can be called. + . repo/utils/DeployUtils.ps1 + # Remove non-release files + Remove-Item -Recurse -Force repo -Include .git* + # Setup the parameters + $Parameters = @{ + AzureKeyVaultUrl = '${{ steps.key-vault-info.outputs.KeyVaultUrl }}' + CertificateName = '${{ steps.key-vault-info.outputs.KeyVaultCertificateName }}' + ModulePath = 'repo/PowerShell/ScubaGear' + GalleryName = $env:GalleryName + } + # This publishes to a private gallery. + Publish-ScubaGearModule @Parameters + azPSVersion: "latest" + - name: Test Module Publish + run: | + Get-Location + $TestContainers = @() + $TestContainers += New-PesterContainer -Path "repo/Testing/Functional/BuildTest" -Data @{ } + $PesterConfig = @{ + Run = @{ + Container = $TestContainers + } + Output = @{ + Verbosity = 'Detailed' + } + } + $Config = New-PesterConfiguration -Hashtable $PesterConfig + Invoke-Pester -Configuration $Config + # This is a manual test that writes the version to the console. + - name: Print Scuba Version + run: | + Install-Module -Name ScubaGear -SkipPublisherCheck + # Import-Module -Name ScubaGear + Invoke-SCuBA -Version diff --git a/.github/workflows/publish_public_package.yaml b/.github/workflows/publish_public_package.yaml new file mode 100644 index 000000000..41d3805cc --- /dev/null +++ b/.github/workflows/publish_public_package.yaml @@ -0,0 +1,106 @@ +# Purpose: Publish on demand to the real gallery (PSGallery). + +name: Publish Public Package + +on: + workflow_dispatch: + inputs: + OverrideModuleVersion: + description: "Override the version of the release. Restricted to SemVer 1.0 - 3 segments" + required: false + type: string + IsPrerelease: + description: "Is this a prerelease" + required: false + type: boolean + default: false + PrereleaseTag: + description: "The prerelease tag: (-)?[0-9A-Za-z]+ (e.g. -alpha1, -rc2, b1234)" + required: false + type: string + # for testing + # push: + # paths: + # - ".github/workflows/publish_public_package.yaml" + # - "utils/DeployUtils.ps1" + +jobs: + publish: + name: Publish to PSGallery + runs-on: windows-latest + environment: Development + permissions: + id-token: write + contents: write + defaults: + run: + shell: powershell + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: repo + - name: Install Azure Signing Tool + run: | + dotnet --version + dotnet tool install --global AzureSignTool --version 4.0.1 + # OIDC Login to Azure Public Cloud with AzPowershell (enableAzPSSession true) + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + - name: Get Key Vault info + id: key-vault-info + env: + KEY_VAULT_INFO: ${{ secrets.SCUBA_KEY_VAULT_PROD}} + run: | + $KeyVaultInfo = ${env:KEY_VAULT_INFO} | ConvertFrom-Json + echo "KeyVaultUrl=$($KeyVaultInfo.KeyVault.URL)" >> $env:GITHUB_OUTPUT + echo "KeyVaultCertificateName=$($KeyVaultInfo.KeyVault.CertificateName)" >> $env:GITHUB_OUTPUT + - name: Sign and Publish Module + run: | + # Source the deploy utilities so the functions in it can be called. + . repo/utils/DeployUtils.ps1 + # Remove non-release files + Remove-Item -Recurse -Force repo -Include .git* + # Extract the API key used to publish to PSGallery + $ApiKey = az keyvault secret show --id '${{ steps.key-vault-info.outputs.KeyVaultUrl }}/secrets/ScubaGear-PSGAllery-API-Key' --query value -o tsv + if (-Not $ApiKey) + { + Write-Error "Failed to retrieve API key" + } + # Setup the parameters + $Parameters = @{ + AzureKeyVaultUrl = '${{ steps.key-vault-info.outputs.KeyVaultUrl }}' + CertificateName = '${{ steps.key-vault-info.outputs.KeyVaultCertificateName }}' + ModulePath = 'repo/PowerShell/ScubaGear' + GalleryName = 'PSGallery' + NuGetApiKey = $ApiKey + } + # if ('true' -eq '${{ inputs.IsPrerelease }}') + # { + # $Parameters.Add('PrereleaseTag', '${{ inputs.PrereleaseTag }}') + # } + # if (-Not [string]::IsNullOrEmpty('${{ inputs.OverrideModuleVersion }}')) + # { + # $Parameters.Add('OverrideModuleVersion', '${{ inputs.OverrideModuleVersion }}') + # } + # This publishes to PSGallery. + Publish-ScubaGearModule @Parameters + # This is a manual test that simply writes the version to the console + - name: Print Scuba Version + run: | + if ('true' -eq '${{ inputs.IsPrerelease }}') + { + $Version = '${{ inputs.OverrideModuleVersion }}' + '${{ inputs.PrereleaseTag }}' + Write-Host "Checking for prerelease with required version: " + $Version + Find-Module -Name ScubaGear -RequiredVersion $Version -AllowPrerelease + } + else + { + Write-Host "Installing latest version" + Find-Module -Name ScubaGear + } diff --git a/.github/workflows/run_pipeline.yaml b/.github/workflows/run_pipeline.yaml index 699eb8450..f0fe4c6b8 100644 --- a/.github/workflows/run_pipeline.yaml +++ b/.github/workflows/run_pipeline.yaml @@ -7,77 +7,25 @@ on: push: pull_request: workflow_dispatch: - inputs: - # When set to true, it will run every step in the pipeline, regardless of - # what files have changed. - doEverything: - description: "Run every workflow in the pipeline." - required: false - type: boolean - default: true jobs: - test-files: - name: Test for Changes - runs-on: ubuntu-latest - # This condition prevents duplicate runs. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - name: Check for Changes - uses: dorny/paths-filter@v3 - id: file-changes - with: - base: ${{ github.ref }} - filters: | - yaml-files: - - added|modified: "**.yml" - - added|modified: "**.yaml" - powershell-files: - - added|modified: "**.ps1" - - added|modified: "**.psm1" - - added|modified: "**.psd1" - - added|modified: "**.pssc" - - added|modified: "**.psrc" - - added|modified: "**.ps1xml" - - added|modified: "**.cdxml" - markdown-files: - - added|modified: "PowerShell/ScubaGear/baselines/*.md" - rego-files: - - added|modified: "**.rego" - outputs: - yaml-changes: ${{ steps.file-changes.outputs.yaml-files || inputs.doEverything }} - powershell-changes: ${{ steps.file-changes.outputs.powershell-files || inputs.doEverything }} - markdown-changes: ${{ steps.file-changes.outputs.markdown-files || inputs.doEverything }} - rego-changes: ${{ steps.file-changes.outputs.rego-files || inputs.doEverything }} lint-yaml: name: Lint - needs: - - test-files - if: needs.test-files.outputs.yaml-changes == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name uses: ./.github/workflows/lint_yaml.yaml lint-powershell: name: Lint - needs: - - test-files - if: needs.test-files.outputs.powershell-changes == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name uses: ./.github/workflows/lint_powershell.yaml syntax: name: Syntax - needs: - - test-files - if: needs.test-files.outputs.markdown-changes == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name uses: ./.github/workflows/syntax_check_markdown.yaml unit-powershell: name: Unit - needs: - - test-files - if: needs.test-files.outputs.powershell-changes == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name uses: ./.github/workflows/unit_test_powershell.yaml unit-opa: name: Unit - needs: - - test-files - if: needs.test-files.outputs.rego-changes == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name uses: ./.github/workflows/unit_test_opa.yaml diff --git a/.github/workflows/run_publish_package.yaml b/.github/workflows/run_publish_package.yaml deleted file mode 100644 index de528cd2c..000000000 --- a/.github/workflows/run_publish_package.yaml +++ /dev/null @@ -1,203 +0,0 @@ -name: Publish Package - -on: - schedule: - - cron: "23 0 * * *" # Execute each day at 00:23 UTC - workflow_dispatch: - inputs: - OverrideModuleVersion: - description: "Override the version of the release. Restricted to SemVer 1.0 - 3 segments" - required: false - type: string - IsPrerelease: - description: "Is this a prerelease" - required: false - type: boolean - default: false - PrereleaseTag: - description: "The prerelease tag: [0-9A-Za-z]+" - required: false - type: string - push: - paths: - - ".github/workflows/run_publish_package.yaml" - - "utils/DeployUtils.ps1" - -env: - GalleryName: PrivateScubaGearGallery - ModuleName: ScubaGear - -jobs: - ReleasePrep: - runs-on: windows-latest - environment: Development - permissions: - id-token: write - contents: write - defaults: - run: - shell: powershell - outputs: - KeyVaultUrl: ${{ steps.key_vault_info.outputs.KeyVaultUrl}} - KeyVaultCertificateName: ${{ steps.key_vault_info.outputs.KeyVaultCertificateName}} - steps: - - name: Get Key Vault info - env: - KEY_VAULT_INFO: ${{ secrets.SCUBA_KEY_VAULT_PROD}} - run: | - $KeyVaultInfo = ${env:KEY_VAULT_INFO} | ConvertFrom-Json - echo "KeyVaultUrl=$($KeyVaultInfo.KeyVault.URL)" >> $env:GITHUB_OUTPUT - echo "KeyVaultCertificateName=$($KeyVaultInfo.KeyVault.CertificateName)" >> $env:GITHUB_OUTPUT - id: "key_vault_info" - - - Publish-Private-Package: - if: github.event.schedule == '23 0 * * *' - needs: [ReleasePrep] - runs-on: windows-latest - environment: Development - permissions: - id-token: write - contents: write - defaults: - run: - shell: powershell - steps: - - name: Install Azure Signing Tool - run: | - dotnet --version - dotnet tool install --global AzureSignTool --version 4.0.1 - - name: Checkout - uses: actions/checkout@v4 - with: - path: repo - - name: OIDC Login to Azure Public Cloud with AzPowershell (enableAzPSSession true) - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - name: Create temporary private gallery - run: | - cd repo - . utils/DeployUtils.ps1 - New-PrivateGallery -GalleryName $env:GalleryName -Trusted - - name: Sign and publish module to private gallery - uses: azure/powershell@v1 - with: - inlineScript: | - . repo/utils/DeployUtils.ps1 - # Remove non-release files - Remove-Item -Recurse -Force repo -Include .git* - Publish-ScubaGearModule ` - -AzureKeyVaultUrl ${{ needs.ReleasePrep.outputs.KeyVaultUrl}} ` - -CertificateName ${{ needs.ReleasePrep.outputs.KeyVaultCertificateName}} ` - -ModulePath repo/PowerShell/ScubaGear ` - -GalleryName $env:GalleryName - azPSVersion: "latest" - - name: Test Module Publish - run: | - Get-Location - $TestContainers = @() - $TestContainers += New-PesterContainer -Path "repo/Testing/Functional/BuildTest" -Data @{ } - $PesterConfig = @{ - Run = @{ - Container = $TestContainers - } - Output = @{ - Verbosity = 'Detailed' - } - } - $Config = New-PesterConfiguration -Hashtable $PesterConfig - Invoke-Pester -Configuration $Config - - name: Check Scuba Version - run: | - Install-Module -Name ScubaGear -SkipPublisherCheck - Import-Module -Name ScubaGear - Invoke-SCuBA -Version - - name: Sign and publish prerelease module to private gallery - uses: azure/powershell@v1 - env: - KEY_VAULT_INFO: ${{ secrets.SCUBA_KEY_VAULT_DEV}} - with: - inlineScript: | - Write-Output "$(pwd)" - . repo/utils/DeployUtils.ps1 - $KeyVaultInfo = ${env:KEY_VAULT_INFO} | ConvertFrom-Json - # Remove non-release files - Remove-Item -Recurse -Force repo -Include .git* - Publish-ScubaGearModule ` - -AzureKeyVaultUrl $($KeyVaultInfo.KeyVault.URL) ` - -CertificateName $($KeyVaultInfo.KeyVault.CertificateName) ` - -ModulePath repo/PowerShell/ScubaGear ` - -GalleryName $env:GalleryName ` - -OverrideModuleVersion '9.9.9' ` - -Prerelease 'alpha' - azPSVersion: "latest" - - name: Check Scuba Version - run: | - $ScubaModules = Find-Module -Name ScubaGear -AllVersions -AllowPrerelease - ($ScubaModules).Version - if ($ScubaModules.Count -eq 2) { - Exit 0 - } - else { - Exit 1 - } - - Publish-To-PSGallery: - if: github.event_name == 'workflow_dispatch' - needs: [ReleasePrep] - runs-on: windows-latest - environment: Development - permissions: - id-token: write - contents: write - defaults: - run: - shell: powershell - steps: - - name: Install Azure Signing Tool - run: | - dotnet --version - dotnet tool install --global AzureSignTool --version 4.0.1 - - name: Checkout - uses: actions/checkout@v4 - with: - path: repo - - name: OIDC Login to Azure Public Cloud with AzPowershell (enableAzPSSession true) - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - name: Sign and publish module to PSGallery - run: | - . repo/utils/DeployUtils.ps1 - # Remove non-release files - Remove-Item -Recurse -Force repo -Include .git* - $ApiKey = az keyvault secret show --id '${{ needs.ReleasePrep.outputs.KeyVaultUrl }}/secrets/ScubaGear-PSGAllery-API-Key' --query value -o tsv - if (-Not $ApiKey){ - Write-Error "Failer to retrieve API key" - } - $PublishSplat = @{ - AzureKeyVaultUrl = '${{ needs.ReleasePrep.outputs.KeyVaultUrl }}' - CertificateName = '${{ needs.ReleasePrep.outputs.KeyVaultCertificateName }}' - ModulePath = 'repo/PowerShell/ScubaGear' - GalleryName = 'PSGallery' - NuGetApiKey = $ApiKey - } - if ('true' -eq '${{ inputs.IsPrerelease }}'){ - $PublishSplat.Add('PrereleaseTag', '${{ inputs.PrereleaseTag }}') - } - if (-Not [string]::IsNullOrEmpty('${{ inputs.OverrideModuleVersion }}')){ - $PublishSplat.Add('OverrideModuleVersion', '${{ inputs.OverrideModuleVersion }}') - } - Publish-ScubaGearModule @PublishSplat - - name: Check Scuba Version - run: | - Install-Module -Name ScubaGear - Import-Module -Name ScubaGear - Invoke-SCuBA -Version diff --git a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Utils/DeployUtils.Tests.ps1 b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Utils/DeployUtils.Tests.ps1 index f364f0517..509539731 100644 --- a/PowerShell/ScubaGear/Testing/Unit/PowerShell/Utils/DeployUtils.Tests.ps1 +++ b/PowerShell/ScubaGear/Testing/Unit/PowerShell/Utils/DeployUtils.Tests.ps1 @@ -107,6 +107,7 @@ Context "Unit Test for ConfigureScubaGearModule" { } Copy-Item -Recurse -Path $ModulePath -Destination $env:TEMP -Force $ManifestPath = Join-Path -Path $env:TEMP -ChildPath 'ScubaGear\ScubaGear.psd1' -Resolve + # 99.1 is an intentionally invalid number Get-Content "$ModulePath\ScubaGear.psd1" | ForEach-Object { $_ -replace '5.1', '99.1' } | Set-Content $ManifestPath Mock -CommandName Write-Error {} } diff --git a/utils/DeployUtils.ps1 b/utils/DeployUtils.ps1 index 220dc0726..410711985 100644 --- a/utils/DeployUtils.ps1 +++ b/utils/DeployUtils.ps1 @@ -16,11 +16,11 @@ function New-PrivateGallery { #> [CmdletBinding(SupportsShouldProcess)] param ( - [Parameter(Mandatory=$false)] - [ValidateScript({Test-Path -Path $_ -IsValid})] + [Parameter(Mandatory = $false)] + [ValidateScript({ Test-Path -Path $_ -IsValid })] [string] $GalleryRootPath = $env:TEMP, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $GalleryName = 'PrivateScubaGearGallery', @@ -29,21 +29,21 @@ function New-PrivateGallery { ) $GalleryPath = Join-Path -Path $GalleryRootPath -ChildPath $GalleryName - if (Test-Path $GalleryPath){ + if (Test-Path $GalleryPath) { Write-Debug "Removing private gallery at $GalleryPath" Remove-Item -Recursive -Force $GalleryPath } New-Item -Path $GalleryPath -ItemType Directory - if (-not (IsRegistered -RepoName $GalleryName)){ + if (-not (IsRegistered -RepoName $GalleryName)) { Write-Debug "Attempting to register $GalleryName repository" $Splat = @{ - Name = $GalleryName - SourceLocation = $GalleryPath - PublishLocation = $GalleryPath - InstallationPolicy = if ($Trusted) {'Trusted'} else {'Untrusted'} + Name = $GalleryName + SourceLocation = $GalleryPath + PublishLocation = $GalleryPath + InstallationPolicy = if ($Trusted) { 'Trusted' } else { 'Untrusted' } } Register-PSRepository @Splat @@ -53,240 +53,290 @@ function New-PrivateGallery { } } -function Publish-ScubaGearModule{ +function Publish-ScubaGearModule { <# .Description Publish ScubaGear module to private package repository + .Parameter AzureKeyVaultUrl + The URL of the key vault with the code signing certificate + .Parameter CertificateName + The name of the code signing certificate .Parameter ModulePath Path to module root directory .Parameter GalleryName Name of the private package repository (i.e., gallery) .Parameter OverrideModuleVersion Optional module version. If provided it will use as module version. Otherwise, the current version from the manifest with a revision number is added instead. + .Parameter PrereleaseTag + The identifier that will be used in place of a version to identify the module in the gallery + .Parameter NuGetApiKey + Specifies the API key that you want to use to publish a module to the online gallery. #> param ( - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$true)] - [ValidateScript({[uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https'})] + [Parameter(Mandatory = $true)] + [ValidateScript({ [uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https' })] [System.Uri] $AzureKeyVaultUrl, - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $CertificateName, - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$true)] - [ValidateScript({Test-Path -Path $_ -PathType Container})] + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] [string] $ModulePath, - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $GalleryName = 'PrivateScubaGearGallery', - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $OverrideModuleVersion = "", - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(ParameterSetName = 'PrivateGallery')] - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $PrereleaseTag = "", - [Parameter(ParameterSetName = 'PublicGallery')] - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $NuGetApiKey ) + Write-Output "Publishing ScubaGear module..." $ModuleBuildPath = Build-ScubaModule -ModulePath $ModulePath -OverrideModuleVersion $OverrideModuleVersion -PrereleaseTag $PrereleaseTag - if (SignScubaGearModule -AzureKeyVaultUrl $AzureKeyVaultUrl -CertificateName $CertificateName -ModulePath $ModuleBuildPath){ - $PublishSplat = @{ - Path = $ModuleBuildPath + Write-Output "The module build path is " + Write-Output $ModuleBuildPath + + if (SignScubaGearModule -AzureKeyVaultUrl $AzureKeyVaultUrl -CertificateName $CertificateName -ModulePath $ModuleBuildPath) { + $Parameters = @{ + Path = $ModuleBuildPath Repository = $GalleryName } - - if ($GalleryName -eq 'PSGallery'){ - $PublishSplat.Add('NuGetApiKey', $NuGetApiKey) + if ($GalleryName -eq 'PSGallery') { + $Parameters.Add('NuGetApiKey', $NuGetApiKey) } - Publish-Module @PublishSplat + Publish-Module @Parameters } else { Write-Error "Failed to sign module." } } -function Build-ScubaModule{ +function Build-ScubaModule { <# .NOTES Internal helper function #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param ( - [Parameter(Mandatory=$true)] - [ValidateScript({Test-Path -Path $_ -PathType Container})] + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] [string] $ModulePath, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $OverrideModuleVersion = "", - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $PrereleaseTag = "" ) + Write-Host "Building ScubaGear module..." + $Leaf = Split-Path -Path $ModulePath -Leaf $ModuleBuildPath = Join-Path -Path $env:TEMP -ChildPath $Leaf - if (Test-Path -Path $ModuleBuildPath -PathType Container){ + if (Test-Path -Path $ModuleBuildPath -PathType Container) { Remove-Item -Recurse -Force $ModuleBuildPath } Copy-Item $ModulePath -Destination $env:TEMP -Recurse - if (-not (ConfigureScubaGearModule -ModulePath $ModuleBuildPath -OverrideModuleVersion $OverrideModuleVersion -PrereleaseTag $PrereleaseTag)){ + if (-not (ConfigureScubaGearModule -ModulePath $ModuleBuildPath -OverrideModuleVersion $OverrideModuleVersion -PrereleaseTag $PrereleaseTag)) { Write-Error "Failed to configure scuba module for publishing." } return $ModuleBuildPath } -function ConfigureScubaGearModule{ +function ConfigureScubaGearModule { <# .NOTES Internal helper function #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param ( - [Parameter(Mandatory=$true)] - [ValidateScript({Test-Path -Path $_ -PathType Container})] + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] $ModulePath, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $OverrideModuleVersion = "", - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $PrereleaseTag = "" ) + Write-Host "Configuring ScubaGear module..." #TODO: Add any module configuration needed (e.g., adjust Module Version) + # Verify that the module path folder exists + if (Test-Path -Path $ModulePath) { + Write-Host "The module dir exists at " + Write-Host $ModulePath + } + else { + Write-Warning "The module dir does not exist at " + Write-Warning $ModulePath + Write-Error "Failing..." + } + $ManifestPath = Join-Path -Path $ModulePath -ChildPath "ScubaGear.psd1" + + # Verify that the manifest file exists + if (Test-Path -Path $ManifestPath) { + Write-Host "The manifest file exists at " + Write-Host $ManifestPath + } + else { + Write-Warning "The manifest file does not exist at " + Write-Warning $ManifestPath + Write-Error "Failing..." + } + $ModuleVersion = $OverrideModuleVersion - if ([string]::IsNullOrEmpty($OverrideModuleVersion)){ + if ([string]::IsNullOrEmpty($OverrideModuleVersion)) { $CurrentModuleVersion = (Import-PowerShellDataFile $ManifestPath).ModuleVersion $TimeStamp = [int32](Get-Date -UFormat %s) $ModuleVersion = "$CurrentModuleVersion.$TimeStamp" } + Write-Host "The prerelease tag is" + Write-Host $PrereleaseTag + Write-Host "The module version is" + Write-Host $ModuleVersion + + $ProjectUri = "https://github.com/cisagov/ScubaGear" + $LicenseUri = "https://github.com/cisagov/ScubaGear/blob/main/LICENSE" # Tags cannot contain spaces + $Tags = 'CISA', 'O365', 'M365', 'AzureAD', 'Configuration', 'Exchange', 'Report', 'Security', 'SharePoint', 'Defender', 'Teams', 'PowerPlatform', 'OneDrive' + $ManifestUpdates = @{ - Path = $ManifestPath + Path = $ManifestPath ModuleVersion = $ModuleVersion - ProjectUri = "https://github.com/cisagov/ScubaGear" - LicenseUri = "https://github.com/cisagov/ScubaGear/blob/main/LICENSE" - Tags = 'CISA', 'O365', 'M365', 'AzureAD', 'Configuration', 'Exchange', 'Report', 'Security', 'SharePoint', 'Defender', 'Teams', 'PowerPlatform', 'OneDrive' + ProjectUri = $ProjectUri + LicenseUri = $LicenseUri + Tags = $Tags } - - if (-Not [string]::IsNullOrEmpty($PrereleaseTag)){ + if (-Not [string]::IsNullOrEmpty($PrereleaseTag)) { $ManifestUpdates.Add('Prerelease', $PrereleaseTag) } try { + $CurrentErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "SilentlyContinue" Update-ModuleManifest @ManifestUpdates + $ErrorActionPreference = $CurrentErrorActionPreference + } + catch { + Write-Warning "Error: Cannot update module manifest:" + Write-Warning "Stacktrace:" + Write-Warning $_.ScriptStackTrace + Write-Warning "Exception:" + Write-Warning $_.Exception + Write-Error "Failed to update module manifest" + return $False + } + try { $CurrentErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "SilentlyContinue" - $Result = Test-ModuleManifest -Path $ManifestPath + Test-ModuleManifest -Path $ManifestPath $ErrorActionPreference = $CurrentErrorActionPreference } catch { - Write-Error "Manifest is not valid" - $Result = $null + Write-Warning "Warning: Cannot test module manifest:" + Write-Warning "Stacktrace:" + Write-Warning $_.ScriptStackTrace + Write-Warning "Exception:" + Write-Warning $_.Exception + Write-Error "Failed to test module manifest" + return $False } - return $null -ne $Result + # True indicates that the updating and testing were successful. + return $True } -function CreateFileList{ +function CreateFileList { <# .NOTES Internal function #> param( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $SourcePath, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [AllowEmptyCollection()] [array] $Extensions = @() ) - $FileNames = @() - - if ($Extensions.Count -gt 0){ + if ($Extensions.Count -gt 0) { $FileNames += Get-ChildItem -Recurse -Path $SourcePath -Include $Extensions } - Write-Debug "Found $($FileNames.Count) files to sign" - $FileList = New-TemporaryFile $FileNames.FullName | Out-File -FilePath $($FileList.FullName) -Encoding utf8 -Force Write-Debug "Files: $(Get-Content $FileList)" return $FileList.FullName } -function CallAzureSignTool{ +function CallAzureSignTool { <# .NOTES Internal function #> param ( - [Parameter(Mandatory=$true)] - [ValidateScript({[uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https'})] + [Parameter(Mandatory = $true)] + [ValidateScript({ [uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https' })] [System.Uri] $AzureKeyVaultUrl, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $CertificateName, - [Parameter(Mandatory=$false)] - [ValidateScript({[uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'http','https'})] + [Parameter(Mandatory = $false)] + [ValidateScript({ [uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'http', 'https' })] $TimeStampServer = 'http://timestamp.digicert.com', - [Parameter(Mandatory=$true)] - [ValidateScript({Test-Path -Path $_ -PathType Leaf})] + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Leaf })] $FileList ) $SignArguments = @( 'sign', '-coe', - '-fd',"sha256", + '-fd', "sha256", '-tr', $TimeStampServer, - '-kvu',$AzureKeyVaultUrl, - '-kvc',$CertificateName, + '-kvu', $AzureKeyVaultUrl, + '-kvc', $CertificateName, '-kvm' - '-ifl',$FileList + '-ifl', $FileList ) - Write-Debug "Calling AzureSignTool: $SignArguments" + Write-Output "Calling AzureSignTool: $SignArguments" $ToolPath = (Get-Command AzureSignTool).Path & $ToolPath $SignArguments } -function SignScubaGearModule{ +function SignScubaGearModule { <# .SYNOPSIS Code sign the specified module @@ -314,36 +364,37 @@ function SignScubaGearModule{ https://github.com/dell/OpenManage-PowerShell-Modules/blob/main/Sign-Module.ps1 #> param ( - [Parameter(Mandatory=$true)] - [ValidateScript({[uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https'})] + [Parameter(Mandatory = $true)] + [ValidateScript({ [uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'https' })] [System.Uri] $AzureKeyVaultUrl, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $CertificateName, - [Parameter(Mandatory=$true)] - [ValidateScript({Test-Path -Path $_ -PathType Container})] + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] $ModulePath, - [Parameter(Mandatory=$false)] - [ValidateScript({[uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'http','https'})] + [Parameter(Mandatory = $false)] + [ValidateScript({ [uri]::IsWellFormedUriString($_, 'Absolute') -and ([uri] $_).Scheme -in 'http', 'https' })] $TimeStampServer = 'http://timestamp.digicert.com' ) + Write-Output "Signing ScubaGear module..." # Digitally sign scripts, manifest, and modules - $FileList = CreateFileList -SourcePath $ModulePath -Extensions "*.ps1","*.psm1","*.psd1" + $FileList = CreateFileList -SourcePath $ModulePath -Extensions "*.ps1", "*.psm1", "*.psd1" CallAzureSignTool ` - -AzureKeyVaultUrl $AzureKeyVaultUrl ` - -CertificateName $CertificateName ` - -TimeStampServer $TimeStampServer ` - -FileList $FileList + -AzureKeyVaultUrl $AzureKeyVaultUrl ` + -CertificateName $CertificateName ` + -TimeStampServer $TimeStampServer ` + -FileList $FileList # Create and sign catalog $CatalogFileName = 'ScubaGear.cat' $CatalogPath = Join-Path -Path $ModulePath -ChildPath $CatalogFileName - if (Test-Path -Path $CatalogPath -PathType Leaf){ + if (Test-Path -Path $CatalogPath -PathType Leaf) { Remove-Item -Path $CatalogPath -Force } @@ -352,22 +403,22 @@ function SignScubaGearModule{ $CatalogPath.FullName | Out-File -FilePath $CatalogList -Encoding utf8 -Force CallAzureSignTool ` - -AzureKeyVaultUrl $AzureKeyVaultUrl ` - -CertificateName $CertificateName ` - -TimeStampServer $TimeStampServer ` - -FileList $CatalogList + -AzureKeyVaultUrl $AzureKeyVaultUrl ` + -CertificateName $CertificateName ` + -TimeStampServer $TimeStampServer ` + -FileList $CatalogList $TestResult = Test-FileCatalog -CatalogFilePath $CatalogPath return 'Valid' -eq $TestResult } -function IsRegistered{ +function IsRegistered { <# .NOTES Internal helper function #> param ( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $RepoName = 'PrivateScubaGearGallery' @@ -375,13 +426,11 @@ function IsRegistered{ Write-Debug "Looking for $RepoName local repository" $Registered = $false - - try{ + try { $Registered = (Get-PSRepository).Name -contains $RepoName } catch { Write-Error "Failed to check IsRegistered: $_" } - return $Registered }